Compare commits

...

10 Commits

Author SHA1 Message Date
github-actions[bot] 60e5ba3a6a 🆙 Bump version to 4.1.11 [skip ci] 2026-05-01 05:49:08 +00:00
DuckieTM 9fa3fad70c Merge pull request #95 from duckietm/dev
🆕 News API
2026-05-01 07:48:08 +02:00
duckietm 860f61f765 🆕 News API 2026-04-30 17:21:33 +02:00
github-actions[bot] c5137bf3dc 🆙 Bump version to 4.1.10 [skip ci] 2026-04-29 15:11:05 +00:00
DuckieTM 5150418796 Merge pull request #94 from duckietm/dev
Dev
2026-04-29 17:10:02 +02:00
duckietm 5c71b318fb 🆙 Latest compiled version 2026-04-29 17:09:43 +02:00
duckietm 1cac407c45 🆕 Effect selection in user dropdown 2026-04-29 13:20:53 +02:00
github-actions[bot] d85eecd624 🆙 Bump version to 4.1.9 [skip ci] 2026-04-28 11:52:58 +00:00
DuckieTM c50098a945 Merge pull request #93 from duckietm/dev
🆕 Added Staffchat to the Emu
2026-04-28 13:52:02 +02:00
duckietm 0224f3f416 🆕 Added Staffchat to the Emu
!!! Do not run the Staffchat plugin anymore !!!!

- execute the sql:

INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES ( 'acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.' )
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment`   = VALUES(`comment`);
2026-04-28 13:51:04 +02:00
8 changed files with 217 additions and 38 deletions
+6 -35
View File
@@ -1,37 +1,3 @@
-- =============================================================================
-- Consolidated Database Updates - All-in-One
-- =============================================================================
-- This file combines ALL individual update scripts from SQL/Database Updates/
-- into a single idempotent migration. Every statement is safe to re-run:
-- - ALTER TABLE ADD COLUMN IF NOT EXISTS (MariaDB 10.0+)
-- - ALTER TABLE CHANGE/MODIFY COLUMN IF EXISTS
-- - CREATE TABLE IF NOT EXISTS
-- - INSERT IGNORE / ON DUPLICATE KEY UPDATE for settings
-- - TRUNCATE + re-insert for reference data (breeding)
--
-- Run order: This file FIRST, then 001_optimize_gameserver.sql
--
-- Source files (in applied order):
-- 1. UpdateDatabase_Allow_diagonale.sql
-- 2. UpdateDatabase_BOT.sql
-- 3. UpdateDatabase_Banners.sql
-- 4. UpdateDatabase_DanceCMD.sql
-- 5. UpdateDatabase_Happiness.sql
-- 6. UpdateDatabase_Websocket.sql
-- 7. UpdateDatabase_unignorable.sql
-- 8. Default_Camera.sql
-- 9. 07012026_UpdateDatabase_to_4-0-1.sql
-- 10. 09012026_UpdateDatabase_to_4-0-2.sql
-- 11. 12012026_Battle Banzai.sql (same as #10, deduplicated)
-- 12. 12012026_Breeding Fixes.sql
-- 13. 12012026_ChatBubbles.sql
-- 14. 16032026_updateall_command.sql
-- 15. 17032026_allow_underpass.sql
-- 16. 19032026_hotel_timezone.sql
-- 17. 21022026_user_prefixes.sql
-- 18. 06042026_builders_club_catalog_offers.sql
-- =============================================================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET @OLD_SQL_MODE = @@SQL_MODE;
@@ -512,8 +478,13 @@ ALTER TABLE `users_settings`
ADD COLUMN IF NOT EXISTS `builders_club_bonus_furni` INT(11) NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`;
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES ( 'acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.' )
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment` = VALUES(`comment`);
-- =============================================================================
-- Done
-- Done.
-- =============================================================================
SET FOREIGN_KEY_CHECKS = 1;
SET SQL_MODE = @OLD_SQL_MODE;
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.8</version>
<version>4.1.11</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -219,6 +219,10 @@ public class Messenger {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
if (habbo.hasPermission(StaffChatBuddy.PERMISSION_KEY)) {
this.friends.putIfAbsent(StaffChatBuddy.BUDDY_ID, new StaffChatBuddy(habbo.getHabboInfo().getId()));
}
}
public MessengerBuddy loadFriend(Habbo habbo, int userId) {
@@ -0,0 +1,57 @@
package com.eu.habbo.habbohotel.messenger;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboGender;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.friends.FriendChatMessageComposer;
public class StaffChatBuddy extends MessengerBuddy {
public static final int BUDDY_ID = -1;
public static final String PERMISSION_KEY = "acc_staff_chat";
public static final String DISPLAY_NAME = "Staff Chat";
public static final String DEFAULT_LOOK = "ADM";
public StaffChatBuddy(int userOne) {
super(BUDDY_ID, DISPLAY_NAME, DEFAULT_LOOK, (short) 0, userOne);
this.setOnline(true);
}
@Override
public void onMessageReceived(Habbo from, String message) {
if (from == null || message == null || message.isEmpty()) return;
// Re-check permission so a staff member who was demoted mid-session
// can no longer broadcast to the staff channel.
if (!from.hasPermission(PERMISSION_KEY)) return;
if (message.charAt(0) == ':') {
CommandHandler.handleCommand(from.getClient(), message);
return;
}
Message chatMessage = new Message(from.getHabboInfo().getId(), BUDDY_ID, message);
Emulator.getGameServer().getGameClientManager().sendBroadcastResponse(
new FriendChatMessageComposer(chatMessage, BUDDY_ID, from.getHabboInfo().getId()).compose(),
PERMISSION_KEY,
from.getClient());
}
@Override
public void serialize(ServerMessage message) {
message.appendInt(this.getId());
message.appendString(this.getUsername());
message.appendInt(this.getGender().equals(HabboGender.M) ? 0 : 1);
message.appendBoolean(true); // online
message.appendBoolean(false); // not in room
message.appendString(this.getLook());
message.appendInt(0); // category
message.appendString(""); // motto
message.appendString(""); // last seen
message.appendString(""); // realname
message.appendBoolean(true); // offline messaging supported
message.appendBoolean(false);
message.appendBoolean(false);
message.appendShort(0); // relation
}
}
@@ -1,14 +1,30 @@
package com.eu.habbo.messages.incoming.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
public class ActivateEffectEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int effectId = this.packet.readInt();
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
if (this.client.getHabbo().getInventory().getEffectsComponent().ownsEffect(effectId)) {
this.client.getHabbo().getInventory().getEffectsComponent().activateEffect(effectId);
if (habbo.getInventory().getEffectsComponent().ownsEffect(effectId)) {
habbo.getInventory().getEffectsComponent().activateEffect(effectId);
return;
}
int rankId = habbo.getHabboInfo().getRank().getId();
if (Emulator.getGameEnvironment().getPermissionsManager().isEffectBlocked(effectId, rankId)) {
return;
}
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room == null || habbo.getHabboInfo().getRiding() != null) return;
room.giveEffect(habbo, effectId, -1);
}
}
@@ -33,6 +33,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
private static final String CHECK_EMAIL_PATH = "/api/auth/check-email";
private static final String CHECK_USERNAME_PATH = "/api/auth/check-username";
private static final String ROOM_TEMPLATES_PATH = "/api/auth/room-templates";
private static final String NEWS_PATH = "/api/auth/news";
private static final String REMEMBER_PATH = "/api/auth/remember";
private static final String REFRESH_PATH = "/api/auth/refresh";
private static final String SERVER_KEY_PATH = "/api/auth/server-key";
@@ -57,6 +58,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
&& !path.equals(FORGOT_PATH) && !path.equals(LOGOUT_PATH)
&& !path.equals(CHECK_EMAIL_PATH) && !path.equals(CHECK_USERNAME_PATH)
&& !path.equals(ROOM_TEMPLATES_PATH)
&& !path.equals(NEWS_PATH)
&& !path.equals(REMEMBER_PATH)
&& !path.equals(REFRESH_PATH)
&& !path.equals(SERVER_KEY_PATH)
@@ -98,6 +100,22 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
return;
}
if (path.equals(NEWS_PATH)) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
return;
}
String ip = resolveClientIp(ctx, req);
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many requests. Try again in " + secs + "s."));
return;
}
handleNews(ctx, req);
return;
}
if (path.equals(SERVER_KEY_PATH)) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
@@ -682,6 +700,76 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
private static final long NEWS_CACHE_TTL_MS = 30_000L;
private static final int NEWS_IMAGE_MAX_BYTES = 512 * 1024;
private static volatile NewsCacheEntry NEWS_CACHE = null;
private static final class NewsCacheEntry {
final byte[] jsonBytes;
final long expiresAt;
NewsCacheEntry(byte[] j, long e) { jsonBytes = j; expiresAt = e; }
}
private void handleNews(ChannelHandlerContext ctx, FullHttpRequest req) {
long now = System.currentTimeMillis();
NewsCacheEntry cached = NEWS_CACHE;
if (cached == null || cached.expiresAt < now) {
JsonArray items = new JsonArray();
int limit = Math.max(1, Math.min(20, Emulator.getConfig().getInt("login.news.limit", 5)));
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, title, body, image, link_text, link_url " +
"FROM ui_news WHERE enabled = 1 " +
"ORDER BY sort_order ASC, id DESC LIMIT ?")) {
stmt.setInt(1, limit);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
int id = rs.getInt("id");
JsonObject n = new JsonObject();
n.addProperty("id", id);
n.addProperty("title", rs.getString("title"));
n.addProperty("body", rs.getString("body"));
String image = rs.getString("image");
if (image != null && image.length() > NEWS_IMAGE_MAX_BYTES) {
LOGGER.warn("ui_news id={} image is {} bytes (>{}KB cap), omitting in response",
id, image.length(), NEWS_IMAGE_MAX_BYTES / 1024);
image = null;
}
n.addProperty("image", image); // gson encodes null as JSON null
n.addProperty("linkText", rs.getString("link_text"));
n.addProperty("linkUrl", rs.getString("link_url"));
items.add(n);
}
}
} catch (Exception e) {
LOGGER.error("ui_news list failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
JsonObject res = new JsonObject();
res.add("news", items);
byte[] bytes = res.toString().getBytes(StandardCharsets.UTF_8);
cached = new NewsCacheEntry(bytes, now + NEWS_CACHE_TTL_MS);
NEWS_CACHE = cached;
}
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.wrappedBuffer(cached.jsonBytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, cached.jsonBytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=30");
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private void handleServerKey(ChannelHandlerContext ctx, FullHttpRequest req) {
try {
JsonObject ok = new JsonObject();