diff --git a/Database Updates/007_PackageRateLimit.sql b/Database Updates/007_PackageRateLimit.sql new file mode 100644 index 00000000..0e6ffbc8 --- /dev/null +++ b/Database Updates/007_PackageRateLimit.sql @@ -0,0 +1,3 @@ +INSERT INTO emulator_settings (`key`, `value`) VALUES ('packet.global.rate.limit', '50'); + +ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `youtube_enabled` TINYINT(1) NOT NULL DEFAULT 0; \ No newline at end of file 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 b361a1c3..af8c9b29 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 @@ -175,7 +175,35 @@ public class Room implements Comparable, ISerialize, Runnable { private volatile boolean muted; private RoomSpecialTypes roomSpecialTypes; private TraxManager traxManager; - + + // YouTube room broadcast state: tracks the current video being broadcast + // by the room owner, the owner's playlist, and which users have the player open. + private boolean youtubeEnabled = false; + private String youtubeCurrentVideo = ""; + private String youtubeSenderName = ""; + private final java.util.List youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>(); + private final java.util.Set youtubeWatchers = java.util.concurrent.ConcurrentHashMap.newKeySet(); + + public boolean isYoutubeEnabled() { return this.youtubeEnabled; } + public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; } + public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; } + public String getYoutubeSenderName() { return this.youtubeSenderName; } + public java.util.List getYoutubePlaylist() { return this.youtubePlaylist; } + public java.util.Set getYoutubeWatchers() { return this.youtubeWatchers; } + + public void setYoutubeVideo(String videoId, String senderName, java.util.List playlist) { + this.youtubeCurrentVideo = videoId; + this.youtubeSenderName = senderName; + this.youtubePlaylist.clear(); + if (playlist != null) this.youtubePlaylist.addAll(playlist); + } + + public void clearYoutubeVideo() { + this.youtubeCurrentVideo = ""; + this.youtubeSenderName = ""; + this.youtubePlaylist.clear(); + } + public final THashMap cache; public Room(ResultSet set) throws SQLException { @@ -203,6 +231,7 @@ public class Room implements Comparable, ISerialize, Runnable { this.allowPetsEat = set.getBoolean("allow_other_pets_eat"); 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; } this.chatMode = set.getInt("chat_mode"); this.chatWeight = set.getInt("chat_weight"); this.chatSpeed = set.getInt("chat_speed"); @@ -1095,7 +1124,7 @@ public class Room implements Comparable, ISerialize, Runnable { if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource() .getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ? WHERE id = ?")) { + "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ? WHERE id = ?")) { statement.setString(1, this.name); statement.setString(2, this.description); statement.setString(3, this.password); @@ -1145,7 +1174,8 @@ public class Room implements Comparable, ISerialize, Runnable { statement.setString(38, this.jukeboxActive ? "1" : "0"); statement.setString(39, this.hideWired ? "1" : "0"); statement.setString(40, this.allowUnderpass ? "1" : "0"); - statement.setInt(41, this.id); + statement.setString(41, this.youtubeEnabled ? "1" : "0"); + statement.setInt(42, this.id); statement.executeUpdate(); this.needsUpdate = false; } catch (SQLException e) { @@ -1774,13 +1804,31 @@ public class Room implements Comparable, ISerialize, Runnable { } public void removeHabbo(Habbo habbo) { + this.cleanupYoutubeWatcher(habbo); this.unitManager.removeHabbo(habbo); } public void removeHabbo(Habbo habbo, boolean sendRemovePacket) { + this.cleanupYoutubeWatcher(habbo); this.unitManager.removeHabbo(habbo, sendRemovePacket); } + private void cleanupYoutubeWatcher(Habbo habbo) { + if (habbo == null) return; + int userId = habbo.getHabboInfo().getId(); + + // If the broadcast sender leaves, stop the broadcast for everyone + if (!this.youtubeCurrentVideo.isEmpty() + && habbo.getHabboInfo().getUsername().equals(this.youtubeSenderName)) { + this.clearYoutubeVideo(); + this.sendComposer(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer("", "", java.util.Collections.emptyList()).compose()); + } + + if (this.youtubeWatchers.remove(userId)) { + this.sendComposer(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomWatchersComposer(this.youtubeWatchers).compose()); + } + } + public void addBot(Bot bot) { this.unitManager.addBot(bot); } 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 2c92c4c3..a65aefa2 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 @@ -790,7 +790,6 @@ public class RoomManager { habbo.getRoomUnit().setInvisible(false); room.addHabbo(habbo); - // Pre-send own wearing badges so the client cache is populated before the user clicks themselves habbo.getClient().sendResponse(new UserBadgesComposer(habbo.getInventory().getBadgesComponent().getWearingBadges(), habbo.getHabboInfo().getId())); List habbos = new ArrayList<>(); @@ -987,6 +986,20 @@ public class RoomManager { } } + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomSettingsComposer( + room.isYoutubeEnabled()).compose()); + + if (!room.getYoutubeCurrentVideo().isEmpty()) { + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer( + room.getYoutubeCurrentVideo(), + room.getYoutubeSenderName(), + room.getYoutubePlaylist()).compose()); + } + if (!room.getYoutubeWatchers().isEmpty()) { + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomWatchersComposer( + room.getYoutubeWatchers()).compose()); + } + WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit()); room.habboEntered(habbo); 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 18163a51..919450b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -677,5 +677,10 @@ public class PacketManager { this.registerHandler(Incoming.GameCenterLeaveGameEvent, GameCenterLeaveGameEvent.class); this.registerHandler(Incoming.GameCenterEvent, GameCenterEvent.class); this.registerHandler(Incoming.GameCenterRequestGameStatusEvent, GameCenterRequestGameStatusEvent.class); + + // YouTube Room Broadcast + this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class); + this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class); + this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.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 9b99e7ab..a85d54fc 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 @@ -436,4 +436,9 @@ public class Incoming { public static final int SetActivePrefixEvent = 7012; public static final int DeletePrefixEvent = 7013; public static final int PurchasePrefixEvent = 7014; + + // YouTube Room Broadcast + public static final int YouTubeRoomPlayEvent = 8001; + public static final int YouTubeRoomWatchingEvent = 8002; + public static final int YouTubeRoomSettingsEvent = 8003; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomPlayEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomPlayEvent.java new file mode 100644 index 00000000..1dec152a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomPlayEvent.java @@ -0,0 +1,63 @@ +package com.eu.habbo.messages.incoming.rooms.youtube; + +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.rooms.youtube.YouTubeRoomBroadcastComposer; + +import java.util.ArrayList; +import java.util.List; + +public class YouTubeRoomPlayEvent extends MessageHandler { + + private static final int MAX_VIDEO_ID_LENGTH = 100; + private static final int MAX_PLAYLIST_ITEM_LENGTH = 200; + private static final int MAX_PLAYLIST_SIZE = 50; + + @Override + public int getRatelimit() { + // Max 1 broadcast every 2 seconds per client + return 2000; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = habbo.getHabboInfo().getCurrentRoom(); + if (room == null) return; + if (!room.isYoutubeEnabled()) return; + if (!room.isOwner(habbo) && !room.hasRights(habbo)) return; + + String videoId = this.packet.readString(); + if (videoId.length() > MAX_VIDEO_ID_LENGTH) { + videoId = videoId.substring(0, MAX_VIDEO_ID_LENGTH); + } + + int playlistCount = this.packet.readInt(); + if (playlistCount > MAX_PLAYLIST_SIZE) playlistCount = MAX_PLAYLIST_SIZE; + if (playlistCount < 0) playlistCount = 0; + + List playlist = new ArrayList<>(); + for (int i = 0; i < playlistCount; i++) { + String item = this.packet.readString(); + if (item.length() > MAX_PLAYLIST_ITEM_LENGTH) { + item = item.substring(0, MAX_PLAYLIST_ITEM_LENGTH); + } + playlist.add(item); + } + + // Store the current video + playlist on the room, or clear if empty + if (videoId.isEmpty()) { + room.clearYoutubeVideo(); + } else { + room.setYoutubeVideo(videoId, habbo.getHabboInfo().getUsername(), playlist); + } + + // Broadcast to everyone in the room (empty videoId = stop) + room.sendComposer( + new YouTubeRoomBroadcastComposer(videoId, habbo.getHabboInfo().getUsername(), playlist).compose() + ); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomSettingsEvent.java new file mode 100644 index 00000000..b3f8ceb5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomSettingsEvent.java @@ -0,0 +1,35 @@ +package com.eu.habbo.messages.incoming.rooms.youtube; + +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.rooms.youtube.YouTubeRoomBroadcastComposer; +import com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomSettingsComposer; + +public class YouTubeRoomSettingsEvent extends MessageHandler { + + @Override + public int getRatelimit() { + return 200; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = habbo.getHabboInfo().getCurrentRoom(); + if (room == null) return; + if (!room.isOwner(habbo)) return; + + boolean enabled = this.packet.readInt() == 1; + room.setYoutubeEnabled(enabled); + room.setNeedsUpdate(true); + room.sendComposer(new YouTubeRoomSettingsComposer(enabled).compose()); + + if (!enabled && !room.getYoutubeCurrentVideo().isEmpty()) { + room.clearYoutubeVideo(); + room.sendComposer(new YouTubeRoomBroadcastComposer("", "", java.util.Collections.emptyList()).compose()); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomWatchingEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomWatchingEvent.java new file mode 100644 index 00000000..3de11651 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomWatchingEvent.java @@ -0,0 +1,39 @@ +package com.eu.habbo.messages.incoming.rooms.youtube; + +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.rooms.youtube.YouTubeRoomWatchersComposer; + +public class YouTubeRoomWatchingEvent extends MessageHandler { + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = habbo.getHabboInfo().getCurrentRoom(); + if (room == null) return; + + boolean watching = this.packet.readInt() == 1; + int userId = habbo.getHabboInfo().getId(); + + boolean changed; + if (watching) { + changed = room.getYoutubeWatchers().add(userId); + } else { + changed = room.getYoutubeWatchers().remove(userId); + } + + if (changed) { + room.sendComposer( + new YouTubeRoomWatchersComposer(room.getYoutubeWatchers()).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 f354973e..386bf3b3 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 @@ -571,4 +571,9 @@ public class Outgoing { public static final int ActivePrefixUpdatedComposer = 7003; public static final int AvailableCommandsComposer = 4050; + // YouTube Room Broadcast + public static final int YouTubeRoomBroadcastComposer = 8001; + public static final int YouTubeRoomWatchersComposer = 8002; + public static final int YouTubeRoomSettingsComposer = 8003; + } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomBroadcastComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomBroadcastComposer.java new file mode 100644 index 00000000..7f8680b3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomBroadcastComposer.java @@ -0,0 +1,31 @@ +package com.eu.habbo.messages.outgoing.rooms.youtube; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +public class YouTubeRoomBroadcastComposer extends MessageComposer { + private final String videoId; + private final String senderName; + private final List playlist; + + public YouTubeRoomBroadcastComposer(String videoId, String senderName, List playlist) { + this.videoId = videoId; + this.senderName = senderName; + this.playlist = playlist; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.YouTubeRoomBroadcastComposer); + this.response.appendString(this.videoId); + this.response.appendString(this.senderName); + this.response.appendInt(this.playlist.size()); + for (String id : this.playlist) { + this.response.appendString(id); + } + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomSettingsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomSettingsComposer.java new file mode 100644 index 00000000..64ff91cb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomSettingsComposer.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.outgoing.rooms.youtube; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class YouTubeRoomSettingsComposer extends MessageComposer { + private final boolean youtubeEnabled; + + public YouTubeRoomSettingsComposer(boolean youtubeEnabled) { + this.youtubeEnabled = youtubeEnabled; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.YouTubeRoomSettingsComposer); + this.response.appendInt(this.youtubeEnabled ? 1 : 0); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomWatchersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomWatchersComposer.java new file mode 100644 index 00000000..d57a3463 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomWatchersComposer.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.outgoing.rooms.youtube; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.Set; + +public class YouTubeRoomWatchersComposer extends MessageComposer { + private final Set watcherIds; + + public YouTubeRoomWatchersComposer(Set watcherIds) { + this.watcherIds = watcherIds; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.YouTubeRoomWatchersComposer); + this.response.appendInt(this.watcherIds.size()); + for (int id : this.watcherIds) { + this.response.appendInt(id); + } + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java index 7a4c8de2..1b83a3ee 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java @@ -6,13 +6,18 @@ import com.eu.habbo.messages.ClientMessage; import com.eu.habbo.networking.gameserver.GameServerAttributes; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; public class GameMessageRateLimit extends MessageToMessageDecoder { + private static final Logger LOGGER = LoggerFactory.getLogger(GameMessageRateLimit.class); + private static final int RESET_TIME = 1; private static final int MAX_COUNTER = 10; + private static final int DEFAULT_GLOBAL_MAX = 50; @Override protected void decode(ChannelHandlerContext ctx, ClientMessage message, List out) throws Exception { @@ -23,26 +28,36 @@ public class GameMessageRateLimit extends MessageToMessageDecoder } int count = 0; + int globalCount = 0; - // Check if reset time has passed. int timestamp = Emulator.getIntUnixTimestamp(); if (timestamp - client.lastPacketCounterCleared > RESET_TIME) { - // Reset counter. client.incomingPacketCounter.clear(); client.lastPacketCounterCleared = timestamp; } else { - // Get stored count for message id. count = client.incomingPacketCounter.getOrDefault(message.getMessageId(), 0); + for (int c : client.incomingPacketCounter.values()) { + globalCount += c; + } } - // If we exceeded the counter, drop the packet. if (count > MAX_COUNTER) { return; } + int globalMax = Emulator.getConfig().getInt("packet.global.rate.limit", DEFAULT_GLOBAL_MAX); + if (globalCount > globalMax) { + if (globalCount == globalMax + 1) { + String username = (client.getHabbo() != null && client.getHabbo().getHabboInfo() != null) + ? client.getHabbo().getHabboInfo().getUsername() : "unauthenticated"; + LOGGER.warn("Global packet rate limit exceeded for {} ({} packets/sec) — dropping excess packets", + username, globalCount); + } + return; + } + client.incomingPacketCounter.put(message.getMessageId(), ++count); - // Continue processing. out.add(message); }