You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Merge branch 'dev' into fix/catalog-inventory-safety
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
CREATE TABLE IF NOT EXISTS `users_earnings_claims` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`category` varchar(64) NOT NULL,
|
||||
`period_key` varchar(32) NOT NULL,
|
||||
`claimed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `users_earnings_claims_unique_period` (`user_id`, `category`, `period_key`),
|
||||
KEY `users_earnings_claims_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('earnings.enabled', '0', 'Enable the emulator-owned earnings center reward hub.'),
|
||||
('earnings.daily_gift.enabled', '1', 'Enable daily gift earnings row.'),
|
||||
('earnings.daily_gift.cooldown.seconds', '86400', 'Cooldown in seconds for daily gift earnings claims.'),
|
||||
('earnings.daily_gift.credits', '0', 'Credits granted by daily gift earnings claims.'),
|
||||
('earnings.daily_gift.pixels', '0', 'Pixels granted by daily gift earnings claims.'),
|
||||
('earnings.daily_gift.points', '0', 'Seasonal points granted by daily gift earnings claims.'),
|
||||
('earnings.daily_gift.points.type', '5', 'Seasonal point type granted by daily gift earnings claims.'),
|
||||
('earnings.games.enabled', '1', 'Enable games earnings row.'),
|
||||
('earnings.games.cooldown.seconds', '86400', 'Cooldown in seconds for games earnings claims.'),
|
||||
('earnings.games.credits', '0', 'Credits granted by games earnings claims.'),
|
||||
('earnings.games.pixels', '0', 'Pixels granted by games earnings claims.'),
|
||||
('earnings.games.points', '0', 'Seasonal points granted by games earnings claims.'),
|
||||
('earnings.games.points.type', '5', 'Seasonal point type granted by games earnings claims.'),
|
||||
('earnings.achievements.enabled', '1', 'Enable achievements earnings row.'),
|
||||
('earnings.achievements.cooldown.seconds', '86400', 'Cooldown in seconds for achievements earnings claims.'),
|
||||
('earnings.achievements.credits', '0', 'Credits granted by achievements earnings claims.'),
|
||||
('earnings.achievements.pixels', '0', 'Pixels granted by achievements earnings claims.'),
|
||||
('earnings.achievements.points', '0', 'Seasonal points granted by achievements earnings claims.'),
|
||||
('earnings.achievements.points.type', '5', 'Seasonal point type granted by achievements earnings claims.'),
|
||||
('earnings.marketplace.enabled', '1', 'Enable marketplace earnings row.'),
|
||||
('earnings.marketplace.cooldown.seconds', '86400', 'Cooldown in seconds for marketplace earnings claims.'),
|
||||
('earnings.marketplace.credits', '0', 'Credits granted by marketplace earnings claims.'),
|
||||
('earnings.marketplace.pixels', '0', 'Pixels granted by marketplace earnings claims.'),
|
||||
('earnings.marketplace.points', '0', 'Seasonal points granted by marketplace earnings claims.'),
|
||||
('earnings.marketplace.points.type', '5', 'Seasonal point type granted by marketplace earnings claims.'),
|
||||
('earnings.hc_payday.enabled', '1', 'Enable HC payday earnings row.'),
|
||||
('earnings.hc_payday.cooldown.seconds', '86400', 'Cooldown in seconds for HC payday earnings claims.'),
|
||||
('earnings.hc_payday.credits', '0', 'Credits granted by HC payday earnings claims.'),
|
||||
('earnings.hc_payday.pixels', '0', 'Pixels granted by HC payday earnings claims.'),
|
||||
('earnings.hc_payday.points', '0', 'Seasonal points granted by HC payday earnings claims.'),
|
||||
('earnings.hc_payday.points.type', '5', 'Seasonal point type granted by HC payday earnings claims.'),
|
||||
('earnings.level_progress.enabled', '1', 'Enable level progress earnings row.'),
|
||||
('earnings.level_progress.cooldown.seconds', '86400', 'Cooldown in seconds for level progress earnings claims.'),
|
||||
('earnings.level_progress.credits', '0', 'Credits granted by level progress earnings claims.'),
|
||||
('earnings.level_progress.pixels', '0', 'Pixels granted by level progress earnings claims.'),
|
||||
('earnings.level_progress.points', '0', 'Seasonal points granted by level progress earnings claims.'),
|
||||
('earnings.level_progress.points.type', '5', 'Seasonal point type granted by level progress earnings claims.'),
|
||||
('earnings.donations.enabled', '1', 'Enable donations earnings row.'),
|
||||
('earnings.donations.cooldown.seconds', '86400', 'Cooldown in seconds for donations earnings claims.'),
|
||||
('earnings.donations.credits', '0', 'Credits granted by donations earnings claims.'),
|
||||
('earnings.donations.pixels', '0', 'Pixels granted by donations earnings claims.'),
|
||||
('earnings.donations.points', '0', 'Seasonal points granted by donations earnings claims.'),
|
||||
('earnings.donations.points.type', '5', 'Seasonal point type granted by donations earnings claims.'),
|
||||
('earnings.bonus_bag.enabled', '1', 'Enable bonus bag earnings row.'),
|
||||
('earnings.bonus_bag.cooldown.seconds', '86400', 'Cooldown in seconds for bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.credits', '0', 'Credits granted by bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.pixels', '0', 'Pixels granted by bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.points', '0', 'Seasonal points granted by bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.points.type', '5', 'Seasonal point type granted by bonus bag earnings claims.'),
|
||||
('earnings.mystery_boxes.enabled', '1', 'Enable mystery boxes earnings row.'),
|
||||
('earnings.mystery_boxes.cooldown.seconds', '86400', 'Cooldown in seconds for mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.credits', '0', 'Credits granted by mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.pixels', '0', 'Pixels granted by mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.points', '0', 'Seasonal points granted by mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.points.type', '5', 'Seasonal point type granted by mystery boxes earnings claims.'),
|
||||
('earnings.club_job.enabled', '1', 'Enable club and job earnings row.'),
|
||||
('earnings.club_job.cooldown.seconds', '86400', 'Cooldown in seconds for club and job earnings claims.'),
|
||||
('earnings.club_job.credits', '0', 'Credits granted by club and job earnings claims.'),
|
||||
('earnings.club_job.pixels', '0', 'Pixels granted by club and job earnings claims.'),
|
||||
('earnings.club_job.points', '0', 'Seasonal points granted by club and job earnings claims.'),
|
||||
('earnings.club_job.points.type', '5', 'Seasonal point type granted by club and job earnings claims.');
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('earnings.daily_gift.badge', '', 'Badge code granted by daily gift earnings claims.'),
|
||||
('earnings.daily_gift.item_id', '0', 'Items base id granted by daily gift earnings claims.'),
|
||||
('earnings.daily_gift.item.quantity', '1', 'Furni quantity granted by daily gift earnings claims.'),
|
||||
('earnings.daily_gift.hc.days', '0', 'HC days granted by daily gift earnings claims.'),
|
||||
('earnings.games.badge', '', 'Badge code granted by games earnings claims.'),
|
||||
('earnings.games.item_id', '0', 'Items base id granted by games earnings claims.'),
|
||||
('earnings.games.item.quantity', '1', 'Furni quantity granted by games earnings claims.'),
|
||||
('earnings.games.hc.days', '0', 'HC days granted by games earnings claims.'),
|
||||
('earnings.achievements.badge', '', 'Badge code granted by achievements earnings claims.'),
|
||||
('earnings.achievements.item_id', '0', 'Items base id granted by achievements earnings claims.'),
|
||||
('earnings.achievements.item.quantity', '1', 'Furni quantity granted by achievements earnings claims.'),
|
||||
('earnings.achievements.hc.days', '0', 'HC days granted by achievements earnings claims.'),
|
||||
('earnings.marketplace.badge', '', 'Badge code granted by marketplace earnings claims.'),
|
||||
('earnings.marketplace.item_id', '0', 'Items base id granted by marketplace earnings claims.'),
|
||||
('earnings.marketplace.item.quantity', '1', 'Furni quantity granted by marketplace earnings claims.'),
|
||||
('earnings.marketplace.hc.days', '0', 'HC days granted by marketplace earnings claims.'),
|
||||
('earnings.hc_payday.badge', '', 'Badge code granted by HC payday earnings claims.'),
|
||||
('earnings.hc_payday.item_id', '0', 'Items base id granted by HC payday earnings claims.'),
|
||||
('earnings.hc_payday.item.quantity', '1', 'Furni quantity granted by HC payday earnings claims.'),
|
||||
('earnings.hc_payday.hc.days', '0', 'HC days granted by HC payday earnings claims.'),
|
||||
('earnings.level_progress.badge', '', 'Badge code granted by level progress earnings claims.'),
|
||||
('earnings.level_progress.item_id', '0', 'Items base id granted by level progress earnings claims.'),
|
||||
('earnings.level_progress.item.quantity', '1', 'Furni quantity granted by level progress earnings claims.'),
|
||||
('earnings.level_progress.hc.days', '0', 'HC days granted by level progress earnings claims.'),
|
||||
('earnings.donations.badge', '', 'Badge code granted by donations earnings claims.'),
|
||||
('earnings.donations.item_id', '0', 'Items base id granted by donations earnings claims.'),
|
||||
('earnings.donations.item.quantity', '1', 'Furni quantity granted by donations earnings claims.'),
|
||||
('earnings.donations.hc.days', '0', 'HC days granted by donations earnings claims.'),
|
||||
('earnings.bonus_bag.badge', '', 'Badge code granted by bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.item_id', '0', 'Items base id granted by bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.item.quantity', '1', 'Furni quantity granted by bonus bag earnings claims.'),
|
||||
('earnings.bonus_bag.hc.days', '0', 'HC days granted by bonus bag earnings claims.'),
|
||||
('earnings.mystery_boxes.badge', '', 'Badge code granted by mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.item_id', '0', 'Items base id granted by mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.item.quantity', '1', 'Furni quantity granted by mystery boxes earnings claims.'),
|
||||
('earnings.mystery_boxes.hc.days', '0', 'HC days granted by mystery boxes earnings claims.'),
|
||||
('earnings.club_job.badge', '', 'Badge code granted by club and job earnings claims.'),
|
||||
('earnings.club_job.item_id', '0', 'Items base id granted by club and job earnings claims.'),
|
||||
('earnings.club_job.item.quantity', '1', 'Furni quantity granted by club and job earnings claims.'),
|
||||
('earnings.club_job.hc.days', '0', 'HC days granted by club and job earnings claims.');
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('earnings.daily_gift.native.enabled', '0', 'Use native hotel subsystem data for daily gift earnings claims when available.'),
|
||||
('earnings.games.native.enabled', '0', 'Use native hotel subsystem data for games earnings claims when available.'),
|
||||
('earnings.achievements.native.enabled', '1', 'Use achievement score thresholds for achievements earnings claims.'),
|
||||
('earnings.marketplace.native.enabled', '1', 'Use marketplace sold item payouts for marketplace earnings claims.'),
|
||||
('earnings.hc_payday.native.enabled', '1', 'Use unclaimed HC payday logs for HC payday earnings claims.'),
|
||||
('earnings.level_progress.native.enabled', '1', 'Use talent track levels for level progress earnings claims.'),
|
||||
('earnings.donations.native.enabled', '0', 'Use native hotel subsystem data for donations earnings claims when available.'),
|
||||
('earnings.bonus_bag.native.enabled', '0', 'Use native hotel subsystem data for bonus bag earnings claims when available.'),
|
||||
('earnings.mystery_boxes.native.enabled', '0', 'Use native hotel subsystem data for mystery boxes earnings claims when available.'),
|
||||
('earnings.club_job.native.enabled', '0', 'Use native hotel subsystem data for club and job earnings claims when available.');
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('earnings.achievements.min_score', '1', 'Minimum achievement score required before achievements earnings can be claimed.'),
|
||||
('earnings.achievements.score.step', '100', 'Achievement score bucket size used to prevent repeated claims for the same progress band.'),
|
||||
('earnings.level_progress.min_level', '1', 'Minimum citizenship/helper talent level required before level progress earnings can be claimed.');
|
||||
@@ -166,6 +166,7 @@ public final class Emulator {
|
||||
Emulator.config.register("rcon.rate_limit.timeout_ms", "0");
|
||||
Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank");
|
||||
Emulator.config.register("rcon.execute_command.allowed_permissions", "");
|
||||
registerEarningsSettings();
|
||||
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||
System.out.println(startupCard(hotelTimezoneId));
|
||||
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
|
||||
@@ -488,6 +489,42 @@ public final class Emulator {
|
||||
return gameServer;
|
||||
}
|
||||
|
||||
private static void registerEarningsSettings() {
|
||||
Emulator.config.register("earnings.enabled", "0");
|
||||
|
||||
String[] categories = {
|
||||
"daily_gift",
|
||||
"games",
|
||||
"achievements",
|
||||
"marketplace",
|
||||
"hc_payday",
|
||||
"level_progress",
|
||||
"donations",
|
||||
"bonus_bag",
|
||||
"mystery_boxes",
|
||||
"club_job"
|
||||
};
|
||||
|
||||
for (String category : categories) {
|
||||
String prefix = "earnings." + category + ".";
|
||||
Emulator.config.register(prefix + "enabled", "1");
|
||||
Emulator.config.register(prefix + "cooldown.seconds", "86400");
|
||||
Emulator.config.register(prefix + "credits", "0");
|
||||
Emulator.config.register(prefix + "pixels", "0");
|
||||
Emulator.config.register(prefix + "points", "0");
|
||||
Emulator.config.register(prefix + "points.type", "5");
|
||||
Emulator.config.register(prefix + "badge", "");
|
||||
Emulator.config.register(prefix + "item_id", "0");
|
||||
Emulator.config.register(prefix + "item.quantity", "1");
|
||||
Emulator.config.register(prefix + "hc.days", "0");
|
||||
Emulator.config.register(prefix + "native.enabled", (category.equals("marketplace") || category.equals("hc_payday") || category.equals("achievements") || category.equals("level_progress")) ? "1" : "0");
|
||||
}
|
||||
|
||||
Emulator.config.register("earnings.achievements.min_score", "1");
|
||||
Emulator.config.register("earnings.achievements.score.step", "100");
|
||||
Emulator.config.register("earnings.level_progress.min_level", "1");
|
||||
}
|
||||
|
||||
public static RCONServer getRconServer() {
|
||||
return rconServer;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
public enum EarningsCategory {
|
||||
DAILY_GIFT("daily_gift"),
|
||||
GAMES("games"),
|
||||
ACHIEVEMENTS("achievements"),
|
||||
MARKETPLACE("marketplace"),
|
||||
HC_PAYDAY("hc_payday"),
|
||||
LEVEL_PROGRESS("level_progress"),
|
||||
DONATIONS("donations"),
|
||||
BONUS_BAG("bonus_bag"),
|
||||
MYSTERY_BOXES("mystery_boxes"),
|
||||
CLUB_JOB("club_job");
|
||||
|
||||
private final String key;
|
||||
|
||||
EarningsCategory(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public static Optional<EarningsCategory> fromKey(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String normalized = key.trim().toLowerCase();
|
||||
return Arrays.stream(values())
|
||||
.filter(category -> category.key.equals(normalized))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionHabboClub;
|
||||
import com.eu.habbo.messages.outgoing.users.AddUserBadgeComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLIntegrityConstraintViolationException;
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class EarningsCenterManager {
|
||||
public static final String CONFIG_PREFIX = "earnings.";
|
||||
private static final int DEFAULT_COOLDOWN_SECONDS = 86400;
|
||||
private static final int DEFAULT_POINTS_TYPE = 5;
|
||||
private static final int MAX_CONFIGURED_REWARD = 1_000_000;
|
||||
private static final int MAX_ITEM_QUANTITY = 100;
|
||||
private static final int MAX_HC_DAYS = 365;
|
||||
|
||||
private final ConfigSource config;
|
||||
private final ClaimRepository claims;
|
||||
private final RewardApplier rewards;
|
||||
private final NativeIntegration nativeIntegration;
|
||||
private final Clock clock;
|
||||
|
||||
public EarningsCenterManager() {
|
||||
this(new EmulatorConfigSource(), new JdbcClaimRepository(), new HabboRewardApplier(), new DefaultNativeIntegration(), Clock.systemUTC());
|
||||
}
|
||||
|
||||
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, Clock clock) {
|
||||
this(config, claims, rewards, new NoopNativeIntegration(), clock);
|
||||
}
|
||||
|
||||
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, NativeIntegration nativeIntegration, Clock clock) {
|
||||
this.config = config;
|
||||
this.claims = claims;
|
||||
this.rewards = rewards;
|
||||
this.nativeIntegration = nativeIntegration;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public List<EarningsEntry> getEntries(Habbo habbo) {
|
||||
int userId = getUserId(habbo);
|
||||
int now = now();
|
||||
List<EarningsEntry> entries = new ArrayList<>();
|
||||
|
||||
for (EarningsCategory category : EarningsCategory.values()) {
|
||||
entries.add(buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public EarningsClaimResult claim(Habbo habbo, String categoryKey) {
|
||||
Optional<EarningsCategory> requestedCategory = EarningsCategory.fromKey(categoryKey);
|
||||
if (requestedCategory.isEmpty()) {
|
||||
return new EarningsClaimResult(null, EarningsClaimResult.Status.UNKNOWN_CATEGORY, null);
|
||||
}
|
||||
|
||||
return claim(habbo, requestedCategory.get());
|
||||
}
|
||||
|
||||
public List<EarningsClaimResult> claimAll(Habbo habbo) {
|
||||
List<EarningsClaimResult> results = new ArrayList<>();
|
||||
|
||||
for (EarningsCategory category : EarningsCategory.values()) {
|
||||
results.add(claim(habbo, category));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private EarningsClaimResult claim(Habbo habbo, EarningsCategory category) {
|
||||
int userId = getUserId(habbo);
|
||||
int now = now();
|
||||
CategoryDefinition definition = loadDefinition(habbo, category);
|
||||
|
||||
if (!definition.enabled()) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.DISABLED, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
if (this.nativeIntegration.handles(category) && nativeEnabled(category)) {
|
||||
return claimNative(habbo, userId, category, now, definition);
|
||||
}
|
||||
|
||||
if (definition.rewards().isEmpty()) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
if (!isEligibleForProgressClaim(habbo, category)) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
String periodKey = periodKey(habbo, category, now, definition.cooldownSeconds());
|
||||
|
||||
try {
|
||||
if (!this.claims.recordClaim(userId, category.getKey(), periodKey, now)) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ALREADY_CLAIMED, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
this.rewards.grant(habbo, definition.rewards());
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(habbo, userId, category, now));
|
||||
} catch (SQLException e) {
|
||||
try {
|
||||
this.claims.removeClaim(userId, category.getKey(), periodKey);
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
}
|
||||
|
||||
private EarningsClaimResult claimNative(Habbo habbo, int userId, EarningsCategory category, int now, CategoryDefinition definition) {
|
||||
try {
|
||||
if (definition.rewards().isEmpty() || !this.nativeIntegration.hasClaim(habbo, category)) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
return this.nativeIntegration.claim(habbo, category)
|
||||
? new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(habbo, userId, category, now))
|
||||
: new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||
} catch (SQLException e) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
}
|
||||
|
||||
private EarningsEntry buildEntry(Habbo habbo, int userId, EarningsCategory category, int now) {
|
||||
CategoryDefinition definition = loadDefinition(habbo, category);
|
||||
boolean claimable = false;
|
||||
int nextClaimAt = 0;
|
||||
|
||||
if (definition.enabled() && !definition.rewards().isEmpty()) {
|
||||
if (this.nativeIntegration.handles(category) && nativeEnabled(category)) {
|
||||
try {
|
||||
claimable = this.nativeIntegration.hasClaim(habbo, category);
|
||||
} catch (SQLException e) {
|
||||
claimable = false;
|
||||
}
|
||||
|
||||
return new EarningsEntry(category, true, claimable, 0, definition.rewards());
|
||||
}
|
||||
|
||||
if (!isEligibleForProgressClaim(habbo, category)) {
|
||||
return new EarningsEntry(category, true, false, 0, definition.rewards());
|
||||
}
|
||||
|
||||
String periodKey = periodKey(habbo, category, now, definition.cooldownSeconds());
|
||||
|
||||
try {
|
||||
claimable = !this.claims.hasClaim(userId, category.getKey(), periodKey);
|
||||
nextClaimAt = claimable ? 0 : nextPeriodStart(now, definition.cooldownSeconds());
|
||||
} catch (SQLException e) {
|
||||
claimable = false;
|
||||
nextClaimAt = nextPeriodStart(now, definition.cooldownSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
return new EarningsEntry(category, definition.enabled(), claimable, nextClaimAt, definition.rewards());
|
||||
}
|
||||
|
||||
private CategoryDefinition loadDefinition(Habbo habbo, EarningsCategory category) {
|
||||
String key = CONFIG_PREFIX + category.getKey() + ".";
|
||||
boolean enabled = this.config.getBoolean(CONFIG_PREFIX + "enabled", false)
|
||||
&& this.config.getBoolean(key + "enabled", true);
|
||||
int cooldown = Math.max(60, this.config.getInt(key + "cooldown.seconds", DEFAULT_COOLDOWN_SECONDS));
|
||||
int pointsType = Math.max(0, this.config.getInt(key + "points.type", DEFAULT_POINTS_TYPE));
|
||||
List<EarningsReward> rewards = new ArrayList<>();
|
||||
|
||||
if (nativeEnabled(category) && this.nativeIntegration.handles(category)) {
|
||||
try {
|
||||
rewards.addAll(this.nativeIntegration.rewards(habbo, category));
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
} else {
|
||||
addReward(rewards, EarningsReward.TYPE_CREDITS, this.config.getInt(key + "credits", 0), 0);
|
||||
addReward(rewards, EarningsReward.TYPE_PIXELS, this.config.getInt(key + "pixels", 0), 0);
|
||||
addReward(rewards, EarningsReward.TYPE_POINTS, this.config.getInt(key + "points", 0), pointsType);
|
||||
addBadgeReward(rewards, this.config.getValue(key + "badge", ""));
|
||||
addItemReward(rewards, this.config.getInt(key + "item_id", 0), this.config.getInt(key + "item.quantity", 1));
|
||||
addHcReward(rewards, this.config.getInt(key + "hc.days", 0));
|
||||
}
|
||||
|
||||
return new CategoryDefinition(enabled, cooldown, rewards);
|
||||
}
|
||||
|
||||
private boolean nativeEnabled(EarningsCategory category) {
|
||||
return this.config.getBoolean(CONFIG_PREFIX + category.getKey() + ".native.enabled", true);
|
||||
}
|
||||
|
||||
private boolean isEligibleForProgressClaim(Habbo habbo, EarningsCategory category) {
|
||||
if (!nativeEnabled(category) || habbo == null || habbo.getHabboStats() == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.ACHIEVEMENTS) {
|
||||
int minimumScore = Math.max(1, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".min_score", 1));
|
||||
return habbo.getHabboStats().getAchievementScore() >= minimumScore;
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.LEVEL_PROGRESS) {
|
||||
int minimumLevel = Math.max(0, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".min_level", 1));
|
||||
int highestLevel = Math.max(habbo.getHabboStats().citizenshipLevel, habbo.getHabboStats().helpersLevel);
|
||||
return highestLevel >= minimumLevel;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addReward(List<EarningsReward> rewards, String type, int amount, int pointsType) {
|
||||
int clampedAmount = Math.min(Math.max(0, amount), MAX_CONFIGURED_REWARD);
|
||||
if (clampedAmount > 0) {
|
||||
rewards.add(new EarningsReward(type, clampedAmount, pointsType));
|
||||
}
|
||||
}
|
||||
|
||||
private void addBadgeReward(List<EarningsReward> rewards, String badgeCode) {
|
||||
if (badgeCode == null || !badgeCode.matches("[A-Za-z0-9_\\-]{1,64}")) {
|
||||
return;
|
||||
}
|
||||
|
||||
rewards.add(new EarningsReward(EarningsReward.TYPE_BADGE, 1, 0, badgeCode));
|
||||
}
|
||||
|
||||
private void addItemReward(List<EarningsReward> rewards, int itemId, int quantity) {
|
||||
if (itemId <= 0 || quantity <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
rewards.add(new EarningsReward(EarningsReward.TYPE_ITEM, Math.min(quantity, MAX_ITEM_QUANTITY), 0, String.valueOf(itemId)));
|
||||
}
|
||||
|
||||
private void addHcReward(List<EarningsReward> rewards, int days) {
|
||||
if (days <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
rewards.add(new EarningsReward(EarningsReward.TYPE_HC_DAYS, Math.min(days, MAX_HC_DAYS), 0));
|
||||
}
|
||||
|
||||
private int getUserId(Habbo habbo) {
|
||||
if (habbo == null || habbo.getHabboInfo() == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return habbo.getHabboInfo().getId();
|
||||
}
|
||||
|
||||
private int now() {
|
||||
return (int) (this.clock.instant().getEpochSecond());
|
||||
}
|
||||
|
||||
private String periodKey(Habbo habbo, EarningsCategory category, int now, int cooldownSeconds) {
|
||||
if (nativeEnabled(category) && habbo != null && habbo.getHabboStats() != null) {
|
||||
if (category == EarningsCategory.ACHIEVEMENTS) {
|
||||
int scoreStep = Math.max(1, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".score.step", 100));
|
||||
return "score:" + (habbo.getHabboStats().getAchievementScore() / scoreStep);
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.LEVEL_PROGRESS) {
|
||||
int highestLevel = Math.max(habbo.getHabboStats().citizenshipLevel, habbo.getHabboStats().helpersLevel);
|
||||
return "level:" + highestLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return String.valueOf(now / cooldownSeconds);
|
||||
}
|
||||
|
||||
private int nextPeriodStart(int now, int cooldownSeconds) {
|
||||
return ((now / cooldownSeconds) + 1) * cooldownSeconds;
|
||||
}
|
||||
|
||||
private record CategoryDefinition(boolean enabled, int cooldownSeconds, List<EarningsReward> rewards) {
|
||||
}
|
||||
|
||||
public interface ConfigSource {
|
||||
boolean getBoolean(String key, boolean defaultValue);
|
||||
|
||||
int getInt(String key, int defaultValue);
|
||||
|
||||
String getValue(String key, String defaultValue);
|
||||
}
|
||||
|
||||
public interface ClaimRepository {
|
||||
boolean hasClaim(int userId, String category, String periodKey) throws SQLException;
|
||||
|
||||
boolean recordClaim(int userId, String category, String periodKey, int claimedAt) throws SQLException;
|
||||
|
||||
void removeClaim(int userId, String category, String periodKey) throws SQLException;
|
||||
}
|
||||
|
||||
public interface RewardApplier {
|
||||
void grant(Habbo habbo, List<EarningsReward> rewards) throws SQLException;
|
||||
}
|
||||
|
||||
public interface NativeIntegration {
|
||||
boolean handles(EarningsCategory category);
|
||||
|
||||
boolean hasClaim(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||
|
||||
List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||
|
||||
boolean claim(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||
}
|
||||
|
||||
private static class EmulatorConfigSource implements ConfigSource {
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defaultValue) {
|
||||
return Emulator.getConfig().getBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defaultValue) {
|
||||
return Emulator.getConfig().getInt(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(String key, String defaultValue) {
|
||||
return Emulator.getConfig().getValue(key, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static class JdbcClaimRepository implements ClaimRepository {
|
||||
@Override
|
||||
public boolean hasClaim(int userId, String category, String periodKey) throws SQLException {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM users_earnings_claims WHERE user_id = ? AND category = ? AND period_key = ? LIMIT 1")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, category);
|
||||
statement.setString(3, periodKey);
|
||||
return statement.executeQuery().next();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recordClaim(int userId, String category, String periodKey, int claimedAt) throws SQLException {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO users_earnings_claims (user_id, category, period_key, claimed_at) VALUES (?, ?, ?, FROM_UNIXTIME(?))")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, category);
|
||||
statement.setString(3, periodKey);
|
||||
statement.setInt(4, claimedAt);
|
||||
return statement.executeUpdate() == 1;
|
||||
} catch (SQLIntegrityConstraintViolationException duplicate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClaim(int userId, String category, String periodKey) throws SQLException {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM users_earnings_claims WHERE user_id = ? AND category = ? AND period_key = ? LIMIT 1")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, category);
|
||||
statement.setString(3, periodKey);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class HabboRewardApplier implements RewardApplier {
|
||||
@Override
|
||||
public void grant(Habbo habbo, List<EarningsReward> rewards) throws SQLException {
|
||||
if (habbo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (EarningsReward reward : rewards) {
|
||||
switch (reward.getType()) {
|
||||
case EarningsReward.TYPE_CREDITS -> habbo.giveCredits(reward.getAmount());
|
||||
case EarningsReward.TYPE_PIXELS -> habbo.givePixels(reward.getAmount());
|
||||
case EarningsReward.TYPE_POINTS -> habbo.givePoints(reward.getPointsType(), reward.getAmount());
|
||||
case EarningsReward.TYPE_BADGE -> grantBadge(habbo, reward.getData());
|
||||
case EarningsReward.TYPE_ITEM -> grantItem(habbo, Integer.parseInt(reward.getData()), reward.getAmount());
|
||||
case EarningsReward.TYPE_HC_DAYS -> grantHcDays(habbo, reward.getAmount());
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void grantBadge(Habbo habbo, String badgeCode) throws SQLException {
|
||||
if (habbo.getInventory().getBadgesComponent().hasBadge(badgeCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
HabboBadge badge = new HabboBadge(0, badgeCode, 0, habbo);
|
||||
badge.run();
|
||||
habbo.getInventory().getBadgesComponent().addBadge(badge);
|
||||
if (habbo.getClient() != null) {
|
||||
habbo.getClient().sendResponse(new AddUserBadgeComposer(badge));
|
||||
}
|
||||
}
|
||||
|
||||
private void grantItem(Habbo habbo, int itemId, int quantity) throws SQLException {
|
||||
if (!itemExists(itemId)) {
|
||||
throw new SQLException("Unknown earnings item reward " + itemId);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) {
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
statement.setInt(2, itemId);
|
||||
statement.addBatch();
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean itemExists(int itemId) throws SQLException {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT id FROM items_base WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, itemId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
return set.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void grantHcDays(Habbo habbo, int days) throws SQLException {
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
int current = habbo.getHabboStats().getClubExpireTimestamp();
|
||||
int newExpire = (current > now ? current : now) + (days * 86400);
|
||||
|
||||
habbo.getHabboStats().setClubExpireTimestamp(newExpire);
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, newExpire);
|
||||
statement.setInt(2, habbo.getHabboInfo().getId());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopNativeIntegration implements NativeIntegration {
|
||||
@Override
|
||||
public boolean handles(EarningsCategory category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(Habbo habbo, EarningsCategory category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claim(Habbo habbo, EarningsCategory category) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultNativeIntegration implements NativeIntegration {
|
||||
@Override
|
||||
public boolean handles(EarningsCategory category) {
|
||||
return category == EarningsCategory.MARKETPLACE || category == EarningsCategory.HC_PAYDAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||
return !rewards(habbo, category).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||
if (habbo == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.MARKETPLACE) {
|
||||
int soldPriceTotal = habbo.getInventory().getSoldPriceTotal();
|
||||
if (soldPriceTotal <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (MarketPlace.MARKETPLACE_CURRENCY == 0) {
|
||||
return List.of(new EarningsReward(EarningsReward.TYPE_CREDITS, soldPriceTotal, 0));
|
||||
}
|
||||
|
||||
return List.of(new EarningsReward(EarningsReward.TYPE_POINTS, soldPriceTotal, MarketPlace.MARKETPLACE_CURRENCY));
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.HC_PAYDAY) {
|
||||
return hcPaydayRewards(habbo);
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claim(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||
if (habbo == null || habbo.getClient() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.MARKETPLACE) {
|
||||
if (habbo.getInventory().getSoldPriceTotal() <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MarketPlace.getCredits(habbo.getClient());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.HC_PAYDAY) {
|
||||
if (hcPaydayRewards(habbo).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SubscriptionHabboClub.processUnclaimed(habbo);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<EarningsReward> hcPaydayRewards(Habbo habbo) throws SQLException {
|
||||
List<EarningsReward> rewards = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT currency, SUM(total_payout) AS amount FROM logs_hc_payday WHERE user_id = ? AND claimed = 0 GROUP BY currency")) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
EarningsReward reward = currencyReward(set.getString("currency"), set.getInt("amount"));
|
||||
if (reward != null) {
|
||||
rewards.add(reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rewards;
|
||||
}
|
||||
|
||||
private EarningsReward currencyReward(String currency, int amount) {
|
||||
if (amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = currency == null ? "" : currency.trim().toLowerCase();
|
||||
return switch (normalized) {
|
||||
case "credits", "credit", "coins", "coin" -> new EarningsReward(EarningsReward.TYPE_CREDITS, amount, 0);
|
||||
case "duckets", "ducket", "pixels", "pixel" -> new EarningsReward(EarningsReward.TYPE_PIXELS, amount, 0);
|
||||
case "diamonds", "diamond" -> new EarningsReward(EarningsReward.TYPE_POINTS, amount, 5);
|
||||
default -> {
|
||||
try {
|
||||
yield new EarningsReward(EarningsReward.TYPE_POINTS, amount, Math.max(0, Integer.parseInt(normalized)));
|
||||
} catch (NumberFormatException e) {
|
||||
yield null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
public class EarningsClaimResult {
|
||||
public enum Status {
|
||||
SUCCESS,
|
||||
DISABLED,
|
||||
UNKNOWN_CATEGORY,
|
||||
ALREADY_CLAIMED,
|
||||
NO_REWARD,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private final EarningsCategory category;
|
||||
private final Status status;
|
||||
private final EarningsEntry entry;
|
||||
|
||||
public EarningsClaimResult(EarningsCategory category, Status status, EarningsEntry entry) {
|
||||
this.category = category;
|
||||
this.status = status;
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public EarningsCategory getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getCategoryKey() {
|
||||
return category == null ? "" : category.getKey();
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return status == Status.SUCCESS;
|
||||
}
|
||||
|
||||
public EarningsEntry getEntry() {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EarningsEntry {
|
||||
private final EarningsCategory category;
|
||||
private final boolean enabled;
|
||||
private final boolean claimable;
|
||||
private final int nextClaimAt;
|
||||
private final List<EarningsReward> rewards;
|
||||
|
||||
public EarningsEntry(EarningsCategory category, boolean enabled, boolean claimable, int nextClaimAt, List<EarningsReward> rewards) {
|
||||
this.category = category;
|
||||
this.enabled = enabled;
|
||||
this.claimable = claimable;
|
||||
this.nextClaimAt = Math.max(0, nextClaimAt);
|
||||
this.rewards = List.copyOf(rewards);
|
||||
}
|
||||
|
||||
public EarningsCategory getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isClaimable() {
|
||||
return claimable;
|
||||
}
|
||||
|
||||
public int getNextClaimAt() {
|
||||
return nextClaimAt;
|
||||
}
|
||||
|
||||
public List<EarningsReward> getRewards() {
|
||||
return rewards;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
public class EarningsReward {
|
||||
public static final String TYPE_CREDITS = "credits";
|
||||
public static final String TYPE_PIXELS = "pixels";
|
||||
public static final String TYPE_POINTS = "points";
|
||||
public static final String TYPE_BADGE = "badge";
|
||||
public static final String TYPE_ITEM = "item";
|
||||
public static final String TYPE_HC_DAYS = "hc_days";
|
||||
|
||||
private final String type;
|
||||
private final int amount;
|
||||
private final int pointsType;
|
||||
private final String data;
|
||||
|
||||
public EarningsReward(String type, int amount, int pointsType) {
|
||||
this(type, amount, pointsType, "");
|
||||
}
|
||||
|
||||
public EarningsReward(String type, int amount, int pointsType, String data) {
|
||||
this.type = type;
|
||||
this.amount = Math.max(0, amount);
|
||||
this.pointsType = Math.max(0, pointsType);
|
||||
this.data = data == null ? "" : data;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public int getPointsType() {
|
||||
return pointsType;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.modtool;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomState;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
@@ -423,12 +424,16 @@ public class ModToolManager {
|
||||
}
|
||||
|
||||
public void kick(Habbo moderator, Habbo target, String message) {
|
||||
if (moderator.hasPermission(Permission.ACC_SUPPORTTOOL) && !target.hasPermission(Permission.ACC_UNKICKABLE)) {
|
||||
if (target.getHabboInfo().getCurrentRoom() != null) {
|
||||
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
|
||||
}
|
||||
this.alert(moderator, target, message, SupportUserAlertedReason.KICKED);
|
||||
if (moderator == null || target == null || !moderator.hasPermission(Permission.ACC_SUPPORTTOOL) ||
|
||||
target.hasPermission(Permission.ACC_UNKICKABLE) ||
|
||||
!canModerateTarget(moderator, target.getHabboInfo().getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.getHabboInfo().getCurrentRoom() != null) {
|
||||
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
|
||||
}
|
||||
this.alert(moderator, target, message, SupportUserAlertedReason.KICKED);
|
||||
}
|
||||
|
||||
public List<ModToolBan> ban(int targetUserId, Habbo moderator, String reason, int duration, ModToolBanType type, int cfhTopic) {
|
||||
@@ -443,7 +448,7 @@ public class ModToolManager {
|
||||
return bans;
|
||||
}
|
||||
|
||||
if (moderator.getHabboInfo().getRank().getId() < offlineInfo.getRank().getId()) {
|
||||
if (!canModerateTarget(moderator, targetUserId)) {
|
||||
return bans;
|
||||
}
|
||||
|
||||
@@ -468,7 +473,7 @@ public class ModToolManager {
|
||||
|
||||
if ((type == ModToolBanType.IP || type == ModToolBanType.SUPER) && target != null && !ban.ip.equals("offline")) {
|
||||
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(ban.ip)) {
|
||||
if (h.getHabboInfo().getRank().getId() >= moderator.getHabboInfo().getRank().getId()) continue;
|
||||
if (!canModerateTarget(moderator, h.getHabboInfo().getId())) continue;
|
||||
|
||||
ban = new ModToolBan(h.getHabboInfo().getId(), h != null ? h.getHabboInfo().getIpLogin() : "offline", h != null ? h.getClient().getMachineId() : "offline", moderator.getHabboInfo().getId(), Emulator.getIntUnixTimestamp() + duration, reason, type, cfhTopic);
|
||||
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
||||
@@ -480,7 +485,7 @@ public class ModToolManager {
|
||||
|
||||
if ((type == ModToolBanType.MACHINE || type == ModToolBanType.SUPER) && target != null && !ban.machineId.equals("offline")) {
|
||||
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithMachineId(ban.machineId)) {
|
||||
if (h.getHabboInfo().getRank().getId() >= moderator.getHabboInfo().getRank().getId()) continue;
|
||||
if (!canModerateTarget(moderator, h.getHabboInfo().getId())) continue;
|
||||
|
||||
ban = new ModToolBan(h.getHabboInfo().getId(), h != null ? h.getHabboInfo().getIpLogin() : "offline", h != null ? h.getClient().getMachineId() : "offline", moderator.getHabboInfo().getId(), Emulator.getIntUnixTimestamp() + duration, reason, type, cfhTopic);
|
||||
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
||||
@@ -501,10 +506,27 @@ public class ModToolManager {
|
||||
if (targetInfo == null)
|
||||
return false;
|
||||
|
||||
return targetInfo.getRank().getId() < moderator.getHabboInfo().getRank().getId();
|
||||
int moderatorRankId = moderator.getHabboInfo().getRank().getId();
|
||||
int targetRankId = targetInfo.getRank().getId();
|
||||
|
||||
return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId;
|
||||
}
|
||||
|
||||
private static boolean isCoreRank(int rankId) {
|
||||
int highestRankId = 0;
|
||||
for (Rank rank : Emulator.getGameEnvironment().getPermissionsManager().getAllRanks()) {
|
||||
highestRankId = Math.max(highestRankId, rank.getId());
|
||||
}
|
||||
|
||||
return highestRankId > 0 && rankId >= highestRankId;
|
||||
}
|
||||
|
||||
public void roomAction(Room room, Habbo moderator, boolean kickUsers, boolean lockDoor, boolean changeTitle) {
|
||||
if (room == null || moderator == null || !moderator.hasPermission(Permission.ACC_SUPPORTTOOL) ||
|
||||
!canModerateTarget(moderator, room.getOwnerId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
SupportRoomActionEvent roomActionEvent = new SupportRoomActionEvent(moderator, room, kickUsers, lockDoor, changeTitle);
|
||||
Emulator.getPluginManager().fireEvent(roomActionEvent);
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ public class ModToolSanctions {
|
||||
}
|
||||
|
||||
public void run(int habboId, Habbo self, int sanctionLevel, int cfhTopic, String reason, int tradeLockedUntil, boolean isMuted, int muteDuration) {
|
||||
if (!ModToolManager.canModerateTarget(self, habboId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sanctionLevel++;
|
||||
|
||||
ModToolSanctionLevelItem sanctionLevelItem = getSanctionLevelItem(sanctionLevel);
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.eu.habbo.messages.incoming.catalog.recycler.RequestRecyclerLogicEvent
|
||||
import com.eu.habbo.messages.incoming.crafting.*;
|
||||
import com.eu.habbo.messages.incoming.events.calendar.AdventCalendarForceOpenEvent;
|
||||
import com.eu.habbo.messages.incoming.events.calendar.AdventCalendarOpenDayEvent;
|
||||
import com.eu.habbo.messages.incoming.earnings.*;
|
||||
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorRequestBlockedTilesEvent;
|
||||
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorRequestDoorSettingsEvent;
|
||||
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorSaveEvent;
|
||||
@@ -130,6 +131,7 @@ public class PacketManager {
|
||||
this.registerCrafting();
|
||||
this.registerCamera();
|
||||
this.registerGameCenter();
|
||||
this.registerEarnings();
|
||||
}
|
||||
|
||||
public PacketNames getNames() {
|
||||
@@ -766,4 +768,10 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class);
|
||||
this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.class);
|
||||
}
|
||||
|
||||
void registerEarnings() throws Exception {
|
||||
this.registerHandler(Incoming.RequestEarningsCenterEvent, RequestEarningsCenterEvent.class);
|
||||
this.registerHandler(Incoming.ClaimEarningsRewardEvent, ClaimEarningsRewardEvent.class);
|
||||
this.registerHandler(Incoming.ClaimAllEarningsRewardsEvent, ClaimAllEarningsRewardsEvent.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,6 +501,9 @@ public class Incoming {
|
||||
public static final int WheelAdminSavePrizesEvent = 9305;
|
||||
public static final int SoundboardPlayEvent = 9306;
|
||||
public static final int SoundboardSetEnabledEvent = 9307;
|
||||
public static final int RequestEarningsCenterEvent = 9308;
|
||||
public static final int ClaimEarningsRewardEvent = 9309;
|
||||
public static final int ClaimAllEarningsRewardsEvent = 9310;
|
||||
public static final int RequestMentionsEvent = 4803;
|
||||
public static final int MarkMentionsReadEvent = 4804;
|
||||
public static final int DeleteMentionEvent = 4805;
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.eu.habbo.messages.incoming.earnings;
|
||||
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.earnings.EarningsClaimResultComposer;
|
||||
|
||||
public class ClaimAllEarningsRewardsEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 3000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() {
|
||||
EarningsCenterManager manager = new EarningsCenterManager();
|
||||
this.client.sendResponse(new EarningsClaimResultComposer(manager.claimAll(this.client.getHabbo())));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.eu.habbo.messages.incoming.earnings;
|
||||
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.earnings.EarningsClaimResultComposer;
|
||||
|
||||
public class ClaimEarningsRewardEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() {
|
||||
String categoryKey = this.packet.readString();
|
||||
EarningsCenterManager manager = new EarningsCenterManager();
|
||||
this.client.sendResponse(new EarningsClaimResultComposer(manager.claim(this.client.getHabbo(), categoryKey)));
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.eu.habbo.messages.incoming.earnings;
|
||||
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.earnings.EarningsCenterComposer;
|
||||
|
||||
public class RequestEarningsCenterEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() {
|
||||
EarningsCenterManager manager = new EarningsCenterManager();
|
||||
this.client.sendResponse(new EarningsCenterComposer(manager.getEntries(this.client.getHabbo())));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.incoming.guilds.forums;
|
||||
|
||||
final class GuildForumInputGuard {
|
||||
static final int MAX_PAGE_LIMIT = 50;
|
||||
static final int MAX_MARK_READ_BATCH = 50;
|
||||
|
||||
private GuildForumInputGuard() {
|
||||
}
|
||||
|
||||
static String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
static boolean isPositiveId(int id) {
|
||||
return id > 0;
|
||||
}
|
||||
|
||||
static boolean isValidPage(int index, int limit) {
|
||||
return index >= 0 && limit > 0 && limit <= MAX_PAGE_LIMIT;
|
||||
}
|
||||
|
||||
static boolean isValidMarkReadBatch(int count) {
|
||||
return count > 0 && count <= MAX_MARK_READ_BATCH;
|
||||
}
|
||||
|
||||
static boolean isSettingsState(int state) {
|
||||
return state >= 0 && state <= 3;
|
||||
}
|
||||
|
||||
static boolean isThreadModerationState(int state) {
|
||||
return state == 1 || state == 10 || state == 20;
|
||||
}
|
||||
|
||||
static boolean isMessageModerationState(int state) {
|
||||
return state == 1 || state == 10 || state == 20;
|
||||
}
|
||||
}
|
||||
+8
@@ -24,11 +24,19 @@ public class GuildForumMarkAsReadEvent extends MessageHandler {
|
||||
int userId = this.client.getHabbo().getHabboInfo().getId();
|
||||
int timestamp = Emulator.getIntUnixTimestamp();
|
||||
|
||||
if (!GuildForumInputGuard.isValidMarkReadBatch(count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
int guildId = this.packet.readInt();
|
||||
this.packet.readInt(); // messageId (not used, we track by timestamp)
|
||||
this.packet.readBoolean(); // isRead
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO `guild_forum_views` (`user_id`, `guild_id`, `timestamp`) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE `timestamp` = ?"
|
||||
|
||||
+9
-1
@@ -28,6 +28,14 @@ public class GuildForumModerateMessageEvent extends MessageHandler {
|
||||
int messageId = packet.readInt();
|
||||
int state = packet.readInt();
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) ||
|
||||
!GuildForumInputGuard.isPositiveId(threadId) ||
|
||||
!GuildForumInputGuard.isPositiveId(messageId) ||
|
||||
!GuildForumInputGuard.isMessageModerationState(state)) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
ForumThread thread = ForumThread.getById(threadId);
|
||||
|
||||
@@ -85,4 +93,4 @@ public class GuildForumModerateMessageEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -36,6 +36,13 @@ public class GuildForumModerateThreadEvent extends MessageHandler {
|
||||
int threadId = packet.readInt();
|
||||
int state = packet.readInt();
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) ||
|
||||
!GuildForumInputGuard.isPositiveId(threadId) ||
|
||||
!GuildForumInputGuard.isThreadModerationState(state)) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
ForumThread thread = ForumThread.getById(threadId);
|
||||
|
||||
@@ -108,4 +115,4 @@ public class GuildForumModerateThreadEvent extends MessageHandler {
|
||||
LOGGER.error("Failed to delete thread " + threadId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -25,8 +25,13 @@ public class GuildForumPostThreadEvent extends MessageHandler {
|
||||
public void handle() throws Exception {
|
||||
int guildId = this.packet.readInt();
|
||||
int threadId = this.packet.readInt();
|
||||
String subject = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
|
||||
String message = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
|
||||
String subject = Emulator.getGameEnvironment().getWordFilter().filter(GuildForumInputGuard.normalize(this.packet.readString()), this.client.getHabbo());
|
||||
String message = Emulator.getGameEnvironment().getWordFilter().filter(GuildForumInputGuard.normalize(this.packet.readString()), this.client.getHabbo());
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) || threadId < 0) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
|
||||
@@ -108,4 +113,4 @@ public class GuildForumPostThreadEvent extends MessageHandler {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -27,6 +27,11 @@ public class GuildForumThreadUpdateEvent extends MessageHandler {
|
||||
boolean isPinned = this.packet.readBoolean();
|
||||
boolean isLocked = this.packet.readBoolean();
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) || !GuildForumInputGuard.isPositiveId(threadId)) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
ForumThread thread = ForumThread.getById(threadId);
|
||||
|
||||
@@ -71,4 +76,4 @@ public class GuildForumThreadUpdateEvent extends MessageHandler {
|
||||
this.client.sendResponse(new GuildForumThreadsComposer(guild, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -22,6 +22,11 @@ public class GuildForumThreadsEvent extends MessageHandler {
|
||||
int guildId = packet.readInt();
|
||||
int index = packet.readInt();
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) || index < 0) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
|
||||
if (guild == null) {
|
||||
|
||||
+7
-1
@@ -29,6 +29,12 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
|
||||
int index = packet.readInt(); // 40
|
||||
int limit = packet.readInt(); // 20
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) ||
|
||||
!GuildForumInputGuard.isPositiveId(threadId) ||
|
||||
!GuildForumInputGuard.isValidPage(index, limit)) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
ForumThread thread = ForumThread.getById(threadId);
|
||||
@@ -59,4 +65,4 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -23,6 +23,15 @@ public class GuildForumUpdateSettingsEvent extends MessageHandler {
|
||||
int postThreads = packet.readInt();
|
||||
int modForum = packet.readInt();
|
||||
|
||||
if (!GuildForumInputGuard.isPositiveId(guildId) ||
|
||||
!GuildForumInputGuard.isSettingsState(canRead) ||
|
||||
!GuildForumInputGuard.isSettingsState(postMessages) ||
|
||||
!GuildForumInputGuard.isSettingsState(postThreads) ||
|
||||
!GuildForumInputGuard.isSettingsState(modForum)) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(400));
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
|
||||
if (guild == null) {
|
||||
@@ -48,4 +57,4 @@ public class GuildForumUpdateSettingsEvent extends MessageHandler {
|
||||
|
||||
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -11,10 +11,17 @@ public class ModToolAlertEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt());
|
||||
int userId = this.packet.readInt();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (alertedUser != null)
|
||||
Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, this.packet.readString(), SupportUserAlertedReason.ALERT);
|
||||
Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, message, SupportUserAlertedReason.ALERT);
|
||||
} else {
|
||||
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.kick").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
}
|
||||
|
||||
+5
-1
@@ -15,9 +15,13 @@ public class ModToolCloseTicketEvent extends MessageHandler {
|
||||
this.packet.readInt();
|
||||
int ticketId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(ticketId) || state < 1 || state > 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId);
|
||||
|
||||
if (issue == null || issue.modId != this.client.getHabbo().getHabboInfo().getId())
|
||||
if (!ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo()))
|
||||
return;
|
||||
|
||||
Habbo sender = Emulator.getGameEnvironment().getHabboManager().getHabbo(issue.senderId);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
final class ModToolInputGuard {
|
||||
static final int MAX_MESSAGE_LENGTH = 1000;
|
||||
|
||||
private ModToolInputGuard() {
|
||||
}
|
||||
|
||||
static String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
static boolean isSafeMessage(String value) {
|
||||
return value != null && !value.isEmpty() && value.length() <= MAX_MESSAGE_LENGTH;
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -14,9 +14,17 @@ public class ModToolIssueChangeTopicEvent extends MessageHandler {
|
||||
this.packet.readInt();
|
||||
int categoryId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(ticketId) || !ModToolTicketGuard.isPositiveId(categoryId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Emulator.getGameEnvironment().getModToolManager().getCfhTopic(categoryId) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId);
|
||||
|
||||
if (issue != null) {
|
||||
if (ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())) {
|
||||
issue.category = categoryId;
|
||||
new UpdateModToolIssue(issue).run();
|
||||
Emulator.getGameEnvironment().getModToolManager().updateTicketToMods(issue);
|
||||
|
||||
+4
@@ -21,6 +21,10 @@ public class ModToolIssueDefaultSanctionEvent extends MessageHandler {
|
||||
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(issueId);
|
||||
|
||||
if (issue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (issue.modId == this.client.getHabbo().getHabboInfo().getId()) {
|
||||
CfhTopic modToolCategory = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(category);
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ public class ModToolKickEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getModToolManager().kick(this.client.getHabbo(), Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt()), this.packet.readString());
|
||||
int userId = this.packet.readInt();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getModToolManager().kick(this.client.getHabbo(), Emulator.getGameEnvironment().getHabboManager().getHabbo(userId), message);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -15,10 +15,17 @@ public class ModToolPickTicketEvent extends MessageHandler {
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
this.packet.readInt();
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(this.packet.readInt());
|
||||
int ticketId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(ticketId)) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("support.ticket.picked.failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId);
|
||||
|
||||
if (issue != null) {
|
||||
if (issue.state == ModToolTicketState.PICKED) {
|
||||
if (!ModToolTicketGuard.canPick(issue)) {
|
||||
this.client.sendResponse(new ModToolIssueInfoComposer(issue));
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("support.ticket.picked.failed"));
|
||||
|
||||
|
||||
+9
-4
@@ -13,17 +13,22 @@ public class ModToolReleaseTicketEvent extends MessageHandler {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
int count = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isValidReleaseBatch(count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (count != 0) {
|
||||
count--;
|
||||
|
||||
int ticketId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(ticketId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId);
|
||||
|
||||
if (issue == null)
|
||||
continue;
|
||||
|
||||
if (issue.modId != this.client.getHabbo().getHabboInfo().getId())
|
||||
if (!ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo()))
|
||||
continue;
|
||||
|
||||
issue.modId = 0;
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
final class ModToolReportInputGuard {
|
||||
static final int MAX_REPORT_MESSAGE_LENGTH = 1000;
|
||||
static final int MAX_PRIVATE_CHAT_LOGS = 100;
|
||||
static final int MAX_PRIVATE_CHAT_MESSAGE_LENGTH = 500;
|
||||
|
||||
private ModToolReportInputGuard() {
|
||||
}
|
||||
|
||||
static String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
static boolean isValidReportMessage(String value) {
|
||||
return value != null && !value.isEmpty() && value.length() <= MAX_REPORT_MESSAGE_LENGTH;
|
||||
}
|
||||
|
||||
static boolean isValidChatLogMessage(String value) {
|
||||
return value != null && value.length() <= MAX_PRIVATE_CHAT_MESSAGE_LENGTH;
|
||||
}
|
||||
|
||||
static boolean isValidPrivateChatLogCount(int count) {
|
||||
return count > 0 && count <= MAX_PRIVATE_CHAT_LOGS;
|
||||
}
|
||||
|
||||
static boolean isPositiveId(int id) {
|
||||
return id > 0;
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -16,7 +16,13 @@ public class ModToolRequestIssueChatlogEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(this.packet.readInt());
|
||||
int ticketId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(ticketId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId);
|
||||
|
||||
if (issue != null) {
|
||||
List<ModToolChatLog> chatlog = new ArrayList<>();
|
||||
|
||||
+7
-1
@@ -12,7 +12,13 @@ public class ModToolRequestRoomChatlogEvent extends MessageHandler {
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
this.packet.readInt();
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.packet.readInt());
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
|
||||
if (room != null)
|
||||
this.client.sendResponse(new ModToolRoomChatlogComposer(room, Emulator.getGameEnvironment().getModToolManager().getRoomChatlog(room.getId())));
|
||||
|
||||
+4
@@ -14,6 +14,10 @@ public class ModToolRequestRoomUserChatlogEvent extends MessageHandler {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (habbo != null) {
|
||||
|
||||
+5
@@ -13,6 +13,11 @@ public class ModToolRequestUserChatlogEvent extends MessageHandler {
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (!ModToolTicketGuard.isPositiveId(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(userId);
|
||||
if (habboInfo == null) {
|
||||
return;
|
||||
|
||||
+6
-1
@@ -11,10 +11,15 @@ public class ModToolRoomAlertEvent extends MessageHandler {
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
this.packet.readInt();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
|
||||
if (room != null) {
|
||||
room.alert(this.packet.readString());
|
||||
room.alert(message);
|
||||
}
|
||||
} else {
|
||||
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.roomalert").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
|
||||
+5
-1
@@ -21,9 +21,13 @@ public class ModToolSanctionAlertEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int userId = this.packet.readInt();
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
int cfhTopic = this.packet.readInt();
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
|
||||
+6
-2
@@ -30,13 +30,17 @@ public class ModToolSanctionBanEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int userId = this.packet.readInt();
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
int cfhTopic = this.packet.readInt();
|
||||
int banType = this.packet.readInt();
|
||||
this.packet.readBoolean();
|
||||
|
||||
int duration = 0;
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (banType) {
|
||||
case BAN_18_HOURS:
|
||||
duration = 18 * 60 * 60;
|
||||
@@ -79,4 +83,4 @@ public class ModToolSanctionBanEvent extends MessageHandler {
|
||||
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -23,9 +23,13 @@ public class ModToolSanctionMuteEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int userId = this.packet.readInt();
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
int cfhTopic = this.packet.readInt();
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
|
||||
+5
-1
@@ -21,10 +21,14 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int userId = this.packet.readInt();
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
int duration = this.packet.readInt();
|
||||
int cfhTopic = this.packet.readInt();
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolIssue;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolTicketState;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
|
||||
final class ModToolTicketGuard {
|
||||
static final int MAX_RELEASE_BATCH = 50;
|
||||
|
||||
private ModToolTicketGuard() {
|
||||
}
|
||||
|
||||
static boolean isPositiveId(int id) {
|
||||
return id > 0;
|
||||
}
|
||||
|
||||
static boolean isValidReleaseBatch(int count) {
|
||||
return count > 0 && count <= MAX_RELEASE_BATCH;
|
||||
}
|
||||
|
||||
static boolean isOwnedBy(ModToolIssue issue, Habbo moderator) {
|
||||
return issue != null && moderator != null && issue.modId == moderator.getHabboInfo().getId();
|
||||
}
|
||||
|
||||
static boolean canPick(ModToolIssue issue) {
|
||||
return issue != null && issue.state != ModToolTicketState.PICKED;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,17 @@ public class ModToolWarnEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt());
|
||||
int userId = this.packet.readInt();
|
||||
String message = ModToolInputGuard.normalize(this.packet.readString());
|
||||
|
||||
if (!ModToolInputGuard.isSafeMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (alertedUser != null)
|
||||
Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, this.packet.readString(), SupportUserAlertedReason.CAUTION);
|
||||
Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, message, SupportUserAlertedReason.CAUTION);
|
||||
} else {
|
||||
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.kick").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.ArrayList;
|
||||
public class ReportBullyEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().getHabboStats().allowTalk()) {
|
||||
if (!this.client.getHabbo().getHabboStats().allowTalk()) {
|
||||
this.client.sendResponse(new HelperRequestDisabledComposer());
|
||||
return;
|
||||
}
|
||||
@@ -22,7 +22,9 @@ public class ReportBullyEvent extends MessageHandler {
|
||||
int userId = this.packet.readInt();
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (userId == this.client.getHabbo().getHabboInfo().getId()) {
|
||||
if (!ModToolReportInputGuard.isPositiveId(userId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(roomId) ||
|
||||
userId == this.client.getHabbo().getHabboInfo().getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -17,7 +17,15 @@ public class ReportCommentEvent extends MessageHandler {
|
||||
int threadId = this.packet.readInt();
|
||||
int commentId = this.packet.readInt();
|
||||
int topicId = this.packet.readInt();
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolReportInputGuard.normalize(this.packet.readString());
|
||||
|
||||
if (!ModToolReportInputGuard.isPositiveId(groupId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(threadId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(commentId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(topicId) ||
|
||||
!ModToolReportInputGuard.isValidReportMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CfhTopic topic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topicId);
|
||||
|
||||
|
||||
@@ -21,12 +21,26 @@ public class ReportEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolReportInputGuard.normalize(this.packet.readString());
|
||||
int topic = this.packet.readInt();
|
||||
int userId = this.packet.readInt();
|
||||
int roomId = this.packet.readInt();
|
||||
this.packet.readInt();
|
||||
|
||||
if (!ModToolReportInputGuard.isValidReportMessage(message) ||
|
||||
topic <= 0 ||
|
||||
(userId != -1 && !ModToolReportInputGuard.isPositiveId(userId)) ||
|
||||
!ModToolReportInputGuard.isPositiveId(roomId) ||
|
||||
userId == this.client.getHabbo().getHabboInfo().getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CfhTopic cfhTopic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topic);
|
||||
|
||||
if (cfhTopic == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
List<ModToolIssue> issues = Emulator.getGameEnvironment().getModToolManager().openTicketsForHabbo(this.client.getHabbo());
|
||||
if (!issues.isEmpty()) {
|
||||
@@ -35,8 +49,6 @@ public class ReportEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
CfhTopic cfhTopic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topic);
|
||||
|
||||
if (userId != -1) {
|
||||
Habbo reported = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
@@ -117,4 +129,4 @@ public class ReportEvent extends MessageHandler {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -21,12 +21,20 @@ public class ReportFriendPrivateChatEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolReportInputGuard.normalize(this.packet.readString());
|
||||
int category = this.packet.readInt();
|
||||
int userId = this.packet.readInt();
|
||||
int count = this.packet.readInt();
|
||||
ArrayList<ModToolChatLog> chatLogs = new ArrayList<>();
|
||||
|
||||
if (!ModToolReportInputGuard.isValidReportMessage(message) ||
|
||||
category <= 0 ||
|
||||
!ModToolReportInputGuard.isPositiveId(userId) ||
|
||||
userId == this.client.getHabbo().getHabboInfo().getId() ||
|
||||
!ModToolReportInputGuard.isValidPrivateChatLogCount(count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo info;
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
if (target != null) {
|
||||
@@ -37,11 +45,17 @@ public class ReportFriendPrivateChatEvent extends MessageHandler {
|
||||
|
||||
if (info == null) return;
|
||||
|
||||
for (int i = 0; i < Math.min(count, 100); i++) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int chatUserId = this.packet.readInt();
|
||||
String username = this.packet.readInt() == info.getId() ? info.getUsername() : this.client.getHabbo().getHabboInfo().getUsername();
|
||||
String chatMessage = ModToolReportInputGuard.normalize(this.packet.readString());
|
||||
|
||||
chatLogs.add(new ModToolChatLog(0, chatUserId, username, this.packet.readString()));
|
||||
if (!ModToolReportInputGuard.isPositiveId(chatUserId) ||
|
||||
!ModToolReportInputGuard.isValidChatLogMessage(chatMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatLogs.add(new ModToolChatLog(0, chatUserId, username, chatMessage));
|
||||
}
|
||||
|
||||
ModToolIssue issue = new ModToolIssue(this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), userId, info.getUsername(), 0, message, ModToolTicketType.IM);
|
||||
|
||||
@@ -28,6 +28,12 @@ public class ReportPhotoEvent extends MessageHandler {
|
||||
int topicId = this.packet.readInt();
|
||||
int itemId = this.packet.readInt();
|
||||
|
||||
if (!ModToolReportInputGuard.isPositiveId(roomId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(topicId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(itemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CfhTopic topic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topicId);
|
||||
|
||||
if (topic == null) return;
|
||||
|
||||
+8
-1
@@ -16,7 +16,14 @@ public class ReportThreadEvent extends MessageHandler {
|
||||
int groupId = this.packet.readInt();
|
||||
int threadId = this.packet.readInt();
|
||||
int topicId = this.packet.readInt();
|
||||
String message = this.packet.readString();
|
||||
String message = ModToolReportInputGuard.normalize(this.packet.readString());
|
||||
|
||||
if (!ModToolReportInputGuard.isPositiveId(groupId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(threadId) ||
|
||||
!ModToolReportInputGuard.isPositiveId(topicId) ||
|
||||
!ModToolReportInputGuard.isValidReportMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CfhTopic topic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topicId);
|
||||
|
||||
|
||||
@@ -607,6 +607,8 @@ public class Outgoing {
|
||||
public static final int WheelAdminPrizesComposer = 9404;
|
||||
public static final int SoundboardSettingsComposer = 9405;
|
||||
public static final int SoundboardPlayComposer = 9406;
|
||||
public static final int EarningsCenterComposer = 9407;
|
||||
public static final int EarningsClaimResultComposer = 9408;
|
||||
public static final int MentionReceivedComposer = 4801;
|
||||
public static final int MentionsListComposer = 4802;
|
||||
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.eu.habbo.messages.outgoing.earnings;
|
||||
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsEntry;
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsReward;
|
||||
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 EarningsCenterComposer extends MessageComposer {
|
||||
private final List<EarningsEntry> entries;
|
||||
|
||||
public EarningsCenterComposer(List<EarningsEntry> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.EarningsCenterComposer);
|
||||
this.response.appendInt(this.entries.size());
|
||||
|
||||
for (EarningsEntry entry : this.entries) {
|
||||
serializeEntry(entry);
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
private void serializeEntry(EarningsEntry entry) {
|
||||
this.response.appendString(entry.getCategory().getKey());
|
||||
this.response.appendBoolean(entry.isEnabled());
|
||||
this.response.appendBoolean(entry.isClaimable());
|
||||
this.response.appendInt(entry.getNextClaimAt());
|
||||
this.response.appendInt(entry.getRewards().size());
|
||||
|
||||
for (EarningsReward reward : entry.getRewards()) {
|
||||
this.response.appendString(reward.getType());
|
||||
this.response.appendInt(reward.getAmount());
|
||||
this.response.appendInt(reward.getPointsType());
|
||||
this.response.appendString(reward.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.eu.habbo.messages.outgoing.earnings;
|
||||
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsClaimResult;
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsEntry;
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsReward;
|
||||
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 EarningsClaimResultComposer extends MessageComposer {
|
||||
private final List<EarningsClaimResult> results;
|
||||
|
||||
public EarningsClaimResultComposer(EarningsClaimResult result) {
|
||||
this.results = List.of(result);
|
||||
}
|
||||
|
||||
public EarningsClaimResultComposer(List<EarningsClaimResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.EarningsClaimResultComposer);
|
||||
this.response.appendInt(this.results.size());
|
||||
|
||||
for (EarningsClaimResult result : this.results) {
|
||||
this.response.appendString(result.getCategoryKey());
|
||||
this.response.appendString(result.getStatus().name().toLowerCase());
|
||||
this.response.appendBoolean(result.isSuccess());
|
||||
serializeEntry(result.getEntry());
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
private void serializeEntry(EarningsEntry entry) {
|
||||
this.response.appendBoolean(entry != null);
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.response.appendString(entry.getCategory().getKey());
|
||||
this.response.appendBoolean(entry.isEnabled());
|
||||
this.response.appendBoolean(entry.isClaimable());
|
||||
this.response.appendInt(entry.getNextClaimAt());
|
||||
this.response.appendInt(entry.getRewards().size());
|
||||
|
||||
for (EarningsReward reward : entry.getRewards()) {
|
||||
this.response.appendString(reward.getType());
|
||||
this.response.appendInt(reward.getAmount());
|
||||
this.response.appendInt(reward.getPointsType());
|
||||
this.response.appendString(reward.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager.ClaimRepository;
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager.ConfigSource;
|
||||
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager.RewardApplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class EarningsCenterManagerTest {
|
||||
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.ofEpochSecond(1_800_000_000L), ZoneOffset.UTC);
|
||||
|
||||
@Test
|
||||
void disabledFeatureReturnsDisabledEntriesAndRejectsClaims() {
|
||||
TestConfig config = new TestConfig().with("earnings.enabled", "0");
|
||||
TestClaims claims = new TestClaims();
|
||||
TestRewards rewards = new TestRewards();
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, claims, rewards, FIXED_CLOCK);
|
||||
|
||||
List<EarningsEntry> entries = manager.getEntries(null);
|
||||
EarningsClaimResult result = manager.claim(null, "daily_gift");
|
||||
|
||||
assertFalse(entries.getFirst().isEnabled());
|
||||
assertFalse(entries.getFirst().isClaimable());
|
||||
assertEquals(EarningsClaimResult.Status.DISABLED, result.getStatus());
|
||||
assertTrue(rewards.granted.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownCategoryIsRejected() {
|
||||
EarningsCenterManager manager = new EarningsCenterManager(enabledConfig(), new TestClaims(), new TestRewards(), FIXED_CLOCK);
|
||||
|
||||
EarningsClaimResult result = manager.claim(null, "not_real");
|
||||
|
||||
assertEquals(EarningsClaimResult.Status.UNKNOWN_CATEGORY, result.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulClaimGrantsConfiguredRewardOnce() {
|
||||
TestConfig config = enabledConfig()
|
||||
.with("earnings.daily_gift.credits", "25")
|
||||
.with("earnings.daily_gift.points", "3")
|
||||
.with("earnings.daily_gift.points.type", "7");
|
||||
TestClaims claims = new TestClaims();
|
||||
TestRewards rewards = new TestRewards();
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, claims, rewards, FIXED_CLOCK);
|
||||
|
||||
EarningsClaimResult first = manager.claim(null, "daily_gift");
|
||||
EarningsClaimResult duplicate = manager.claim(null, "daily_gift");
|
||||
|
||||
assertEquals(EarningsClaimResult.Status.SUCCESS, first.getStatus());
|
||||
assertEquals(EarningsClaimResult.Status.ALREADY_CLAIMED, duplicate.getStatus());
|
||||
assertEquals(2, rewards.granted.size());
|
||||
assertEquals(EarningsReward.TYPE_CREDITS, rewards.granted.get(0).getType());
|
||||
assertEquals(25, rewards.granted.get(0).getAmount());
|
||||
assertEquals(EarningsReward.TYPE_POINTS, rewards.granted.get(1).getType());
|
||||
assertEquals(7, rewards.granted.get(1).getPointsType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void categoryWithNoConfiguredRewardIsNotClaimable() {
|
||||
EarningsCenterManager manager = new EarningsCenterManager(enabledConfig(), new TestClaims(), new TestRewards(), FIXED_CLOCK);
|
||||
|
||||
EarningsClaimResult result = manager.claim(null, "games");
|
||||
|
||||
assertEquals(EarningsClaimResult.Status.NO_REWARD, result.getStatus());
|
||||
assertFalse(result.getEntry().isClaimable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurableBadgeItemAndHcRewardsAreIncludedInEntryState() {
|
||||
TestConfig config = enabledConfig()
|
||||
.with("earnings.bonus_bag.badge", "ACH_Test1")
|
||||
.with("earnings.bonus_bag.item_id", "123")
|
||||
.with("earnings.bonus_bag.item.quantity", "2")
|
||||
.with("earnings.bonus_bag.hc.days", "7");
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, new TestClaims(), new TestRewards(), FIXED_CLOCK);
|
||||
|
||||
EarningsEntry entry = manager.getEntries(null).stream()
|
||||
.filter(current -> current.getCategory() == EarningsCategory.BONUS_BAG)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertTrue(entry.isClaimable());
|
||||
assertEquals(3, entry.getRewards().size());
|
||||
assertEquals(EarningsReward.TYPE_BADGE, entry.getRewards().get(0).getType());
|
||||
assertEquals("ACH_Test1", entry.getRewards().get(0).getData());
|
||||
assertEquals(EarningsReward.TYPE_ITEM, entry.getRewards().get(1).getType());
|
||||
assertEquals("123", entry.getRewards().get(1).getData());
|
||||
assertEquals(2, entry.getRewards().get(1).getAmount());
|
||||
assertEquals(EarningsReward.TYPE_HC_DAYS, entry.getRewards().get(2).getType());
|
||||
assertEquals(7, entry.getRewards().get(2).getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedRewardGrantRollsBackClaimRecord() {
|
||||
TestConfig config = enabledConfig().with("earnings.daily_gift.credits", "10");
|
||||
TestClaims claims = new TestClaims();
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, claims, (habbo, rewards) -> {
|
||||
throw new SQLException("grant failed");
|
||||
}, FIXED_CLOCK);
|
||||
|
||||
EarningsClaimResult failed = manager.claim(null, "daily_gift");
|
||||
EarningsClaimResult retried = new EarningsCenterManager(config, claims, new TestRewards(), FIXED_CLOCK)
|
||||
.claim(null, "daily_gift");
|
||||
|
||||
assertEquals(EarningsClaimResult.Status.ERROR, failed.getStatus());
|
||||
assertEquals(EarningsClaimResult.Status.SUCCESS, retried.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nativeMarketplaceRowsUseNativeClaimInsteadOfPeriodicClaimLedger() {
|
||||
TestConfig config = enabledConfig().with("earnings.marketplace.native.enabled", "1");
|
||||
TestClaims claims = new TestClaims();
|
||||
TestNativeIntegration nativeIntegration = new TestNativeIntegration(EarningsCategory.MARKETPLACE)
|
||||
.withReward(new EarningsReward(EarningsReward.TYPE_CREDITS, 45, 0));
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, claims, new TestRewards(), nativeIntegration, FIXED_CLOCK);
|
||||
|
||||
EarningsEntry entry = manager.getEntries(null).stream()
|
||||
.filter(current -> current.getCategory() == EarningsCategory.MARKETPLACE)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
EarningsClaimResult result = manager.claim(null, "marketplace");
|
||||
|
||||
assertTrue(entry.isClaimable());
|
||||
assertEquals(45, entry.getRewards().getFirst().getAmount());
|
||||
assertEquals(EarningsClaimResult.Status.SUCCESS, result.getStatus());
|
||||
assertEquals(1, nativeIntegration.claims);
|
||||
assertTrue(claims.claims.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nativeRowsWithoutAvailableRewardsAreNotClaimable() {
|
||||
TestConfig config = enabledConfig().with("earnings.hc_payday.native.enabled", "1");
|
||||
TestNativeIntegration nativeIntegration = new TestNativeIntegration(EarningsCategory.HC_PAYDAY);
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, new TestClaims(), new TestRewards(), nativeIntegration, FIXED_CLOCK);
|
||||
|
||||
EarningsEntry entry = manager.getEntries(null).stream()
|
||||
.filter(current -> current.getCategory() == EarningsCategory.HC_PAYDAY)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
EarningsClaimResult result = manager.claim(null, "hc_payday");
|
||||
|
||||
assertFalse(entry.isClaimable());
|
||||
assertEquals(EarningsClaimResult.Status.NO_REWARD, result.getStatus());
|
||||
assertEquals(0, nativeIntegration.claims);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimAllGrantsClaimableRowsAndSkipsAlreadyClaimedRows() throws SQLException {
|
||||
TestConfig config = enabledConfig()
|
||||
.with("earnings.daily_gift.credits", "10")
|
||||
.with("earnings.games.pixels", "4");
|
||||
TestClaims claims = new TestClaims();
|
||||
TestRewards rewards = new TestRewards();
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, claims, rewards, FIXED_CLOCK);
|
||||
|
||||
claims.recordClaim(0, "daily_gift", String.valueOf(1_800_000_000L / 86400), 1_800_000_000);
|
||||
List<EarningsClaimResult> results = manager.claimAll(null);
|
||||
|
||||
assertEquals(EarningsClaimResult.Status.ALREADY_CLAIMED, results.get(0).getStatus());
|
||||
assertEquals(EarningsClaimResult.Status.SUCCESS, results.get(1).getStatus());
|
||||
assertEquals(1, rewards.granted.size());
|
||||
assertEquals(EarningsReward.TYPE_PIXELS, rewards.granted.getFirst().getType());
|
||||
assertEquals(4, rewards.granted.getFirst().getAmount());
|
||||
}
|
||||
|
||||
private static TestConfig enabledConfig() {
|
||||
return new TestConfig().with("earnings.enabled", "1");
|
||||
}
|
||||
|
||||
private static class TestConfig implements ConfigSource {
|
||||
private final Map<String, String> values = new HashMap<>();
|
||||
|
||||
TestConfig with(String key, String value) {
|
||||
this.values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defaultValue) {
|
||||
return this.values.getOrDefault(key, defaultValue ? "1" : "0").equals("1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defaultValue) {
|
||||
return Integer.parseInt(this.values.getOrDefault(key, String.valueOf(defaultValue)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(String key, String defaultValue) {
|
||||
return this.values.getOrDefault(key, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestClaims implements ClaimRepository {
|
||||
private final Set<String> claims = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(int userId, String category, String periodKey) {
|
||||
return this.claims.contains(key(userId, category, periodKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recordClaim(int userId, String category, String periodKey, int claimedAt) {
|
||||
return this.claims.add(key(userId, category, periodKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClaim(int userId, String category, String periodKey) {
|
||||
this.claims.remove(key(userId, category, periodKey));
|
||||
}
|
||||
|
||||
private String key(int userId, String category, String periodKey) {
|
||||
return userId + ":" + category + ":" + periodKey;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestRewards implements RewardApplier {
|
||||
private final List<EarningsReward> granted = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void grant(com.eu.habbo.habbohotel.users.Habbo habbo, List<EarningsReward> rewards) {
|
||||
this.granted.addAll(rewards);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestNativeIntegration implements EarningsCenterManager.NativeIntegration {
|
||||
private final EarningsCategory category;
|
||||
private final List<EarningsReward> rewards = new ArrayList<>();
|
||||
private int claims = 0;
|
||||
|
||||
private TestNativeIntegration(EarningsCategory category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
private TestNativeIntegration withReward(EarningsReward reward) {
|
||||
this.rewards.add(reward);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(EarningsCategory category) {
|
||||
return this.category == category;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
|
||||
return handles(category) && !this.rewards.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EarningsReward> rewards(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
|
||||
return handles(category) ? List.copyOf(this.rewards) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claim(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
|
||||
if (!hasClaim(habbo, category)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.claims++;
|
||||
this.rewards.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.eu.habbo.messages.incoming.guilds.forums;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GuildForumInputGuardContractTest {
|
||||
@Test
|
||||
void forumHandlersValidateClientProvidedIds() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums");
|
||||
|
||||
for (String handler : List.of(
|
||||
"GuildForumPostThreadEvent.java",
|
||||
"GuildForumModerateMessageEvent.java",
|
||||
"GuildForumModerateThreadEvent.java",
|
||||
"GuildForumThreadUpdateEvent.java",
|
||||
"GuildForumThreadsEvent.java",
|
||||
"GuildForumThreadsMessagesEvent.java",
|
||||
"GuildForumMarkAsReadEvent.java",
|
||||
"GuildForumUpdateSettingsEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("GuildForumInputGuard.isPositiveId"),
|
||||
handler + " must reject zero or negative client-provided ids");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void forumHandlersBoundExpensiveClientInputs() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums");
|
||||
|
||||
String messages = Files.readString(base.resolve("GuildForumThreadsMessagesEvent.java"));
|
||||
String markRead = Files.readString(base.resolve("GuildForumMarkAsReadEvent.java"));
|
||||
String settings = Files.readString(base.resolve("GuildForumUpdateSettingsEvent.java"));
|
||||
String moderateThread = Files.readString(base.resolve("GuildForumModerateThreadEvent.java"));
|
||||
String moderateMessage = Files.readString(base.resolve("GuildForumModerateMessageEvent.java"));
|
||||
|
||||
assertTrue(messages.contains("GuildForumInputGuard.isValidPage(index, limit)"),
|
||||
"thread message reads must bound index/limit before fetching comments");
|
||||
assertTrue(markRead.contains("GuildForumInputGuard.isValidMarkReadBatch(count)"),
|
||||
"mark-as-read must bound the client-provided batch count before DB writes");
|
||||
assertTrue(settings.contains("GuildForumInputGuard.isSettingsState"),
|
||||
"forum settings must reject unknown SettingsState values");
|
||||
assertTrue(moderateThread.contains("GuildForumInputGuard.isThreadModerationState(state)"),
|
||||
"thread moderation must reject unknown ForumThreadState values");
|
||||
assertTrue(moderateMessage.contains("GuildForumInputGuard.isMessageModerationState(state)"),
|
||||
"message moderation must reject unknown ForumThreadState values");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forumPostsNormalizeTextBeforeFiltering() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java"));
|
||||
|
||||
assertTrue(source.contains("GuildForumInputGuard.normalize(this.packet.readString())"),
|
||||
"forum post subject and body should be normalized before word filtering and length checks");
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.eu.habbo.messages.incoming.guilds.forums;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GuildForumInputGuardTest {
|
||||
@Test
|
||||
void normalizesNullableText() {
|
||||
assertEquals("", GuildForumInputGuard.normalize(null));
|
||||
assertEquals("hello", GuildForumInputGuard.normalize(" hello "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesIdsAndPaging() {
|
||||
assertFalse(GuildForumInputGuard.isPositiveId(0));
|
||||
assertTrue(GuildForumInputGuard.isPositiveId(1));
|
||||
assertFalse(GuildForumInputGuard.isValidPage(-1, 20));
|
||||
assertFalse(GuildForumInputGuard.isValidPage(0, 0));
|
||||
assertTrue(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT));
|
||||
assertFalse(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesBatchAndStates() {
|
||||
assertFalse(GuildForumInputGuard.isValidMarkReadBatch(0));
|
||||
assertTrue(GuildForumInputGuard.isValidMarkReadBatch(GuildForumInputGuard.MAX_MARK_READ_BATCH));
|
||||
assertFalse(GuildForumInputGuard.isValidMarkReadBatch(GuildForumInputGuard.MAX_MARK_READ_BATCH + 1));
|
||||
|
||||
assertTrue(GuildForumInputGuard.isSettingsState(0));
|
||||
assertTrue(GuildForumInputGuard.isSettingsState(3));
|
||||
assertFalse(GuildForumInputGuard.isSettingsState(4));
|
||||
|
||||
assertTrue(GuildForumInputGuard.isThreadModerationState(20));
|
||||
assertFalse(GuildForumInputGuard.isThreadModerationState(999));
|
||||
assertTrue(GuildForumInputGuard.isMessageModerationState(10));
|
||||
assertFalse(GuildForumInputGuard.isMessageModerationState(0));
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ModToolInputGuardTest {
|
||||
@Test
|
||||
void normalizesNullableMessages() {
|
||||
assertEquals("", ModToolInputGuard.normalize(null));
|
||||
assertEquals("warn", ModToolInputGuard.normalize(" warn "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void staffMessagesMustBeNonEmptyAndBounded() {
|
||||
assertFalse(ModToolInputGuard.isSafeMessage(null));
|
||||
assertFalse(ModToolInputGuard.isSafeMessage(""));
|
||||
assertTrue(ModToolInputGuard.isSafeMessage("a".repeat(ModToolInputGuard.MAX_MESSAGE_LENGTH)));
|
||||
assertFalse(ModToolInputGuard.isSafeMessage("a".repeat(ModToolInputGuard.MAX_MESSAGE_LENGTH + 1)));
|
||||
}
|
||||
}
|
||||
+49
-2
@@ -45,7 +45,7 @@ class ModToolPermissionContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void modToolSanctionsCannotTargetSameOrHigherRanks() throws Exception {
|
||||
void modToolSanctionsCannotTargetPeerRanksUnlessOperatorIsCoreRank() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
@@ -60,6 +60,53 @@ class ModToolPermissionContractTest {
|
||||
|
||||
String manager = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java"));
|
||||
assertTrue(manager.contains("!canModerateTarget(moderator, target.getHabboInfo().getId())"),
|
||||
"ModToolManager.alert must refuse alerts/warnings against same-or-higher-rank targets");
|
||||
"ModToolManager.alert must refuse alerts/warnings against protected targets");
|
||||
assertTrue(manager.contains("targetRankId < moderatorRankId"),
|
||||
"non-core moderators must only target lower-ranked users");
|
||||
assertTrue(manager.contains("isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId"),
|
||||
"highest/core moderators should be allowed to target peer ranks");
|
||||
assertTrue(manager.contains("private static boolean isCoreRank(int rankId)"),
|
||||
"core-rank detection should be centralized in ModToolManager");
|
||||
}
|
||||
|
||||
@Test
|
||||
void managerEntryPointsShareTargetAndRoomOwnerGuards() throws Exception {
|
||||
String manager = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java"));
|
||||
String sanctions = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java"));
|
||||
String defaultSanction = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java"));
|
||||
|
||||
assertTrue(manager.contains("!canModerateTarget(moderator, targetUserId)"),
|
||||
"ModToolManager.ban must use the central target-rank guard for offline and online users");
|
||||
assertTrue(manager.contains("!canModerateTarget(moderator, h.getHabboInfo().getId())"),
|
||||
"IP and machine fan-out bans must skip protected peer-or-higher ranked sessions");
|
||||
assertTrue(manager.contains("!canModerateTarget(moderator, room.getOwnerId())"),
|
||||
"ModToolManager.roomAction must refuse mutations on rooms owned by protected ranks");
|
||||
assertTrue(sanctions.contains("!ModToolManager.canModerateTarget(self, habboId)"),
|
||||
"ModToolSanctions.run must guard every sanction path before writing or applying it");
|
||||
assertTrue(defaultSanction.contains("if (issue == null)"),
|
||||
"default sanctions must tolerate stale or missing ticket ids");
|
||||
}
|
||||
|
||||
@Test
|
||||
void staffSuppliedModToolMessagesAreBounded() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
"ModToolAlertEvent.java",
|
||||
"ModToolWarnEvent.java",
|
||||
"ModToolKickEvent.java",
|
||||
"ModToolRoomAlertEvent.java",
|
||||
"ModToolSanctionAlertEvent.java",
|
||||
"ModToolSanctionBanEvent.java",
|
||||
"ModToolSanctionMuteEvent.java",
|
||||
"ModToolSanctionTradeLockEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("ModToolInputGuard.normalize"),
|
||||
handler + " must normalize staff-supplied text before use");
|
||||
assertTrue(source.contains("ModToolInputGuard.isSafeMessage"),
|
||||
handler + " must reject empty or oversized staff-supplied text");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ModToolReportInputContractTest {
|
||||
@Test
|
||||
void reportHandlersNormalizeAndBoundFreeText() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
"ReportEvent.java",
|
||||
"ReportFriendPrivateChatEvent.java",
|
||||
"ReportCommentEvent.java",
|
||||
"ReportThreadEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("ModToolReportInputGuard.normalize"),
|
||||
handler + " must normalize report text before persistence or staff broadcast");
|
||||
assertTrue(source.contains("ModToolReportInputGuard.isValidReportMessage"),
|
||||
handler + " must reject empty or oversized report text");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportHandlersRejectInvalidIdsAndCounts() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
"ReportEvent.java",
|
||||
"ReportFriendPrivateChatEvent.java",
|
||||
"ReportCommentEvent.java",
|
||||
"ReportThreadEvent.java",
|
||||
"ReportBullyEvent.java",
|
||||
"ReportPhotoEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("ModToolReportInputGuard.isPositiveId"),
|
||||
handler + " must reject zero or negative ids supplied by the client");
|
||||
}
|
||||
|
||||
String privateChat = Files.readString(base.resolve("ReportFriendPrivateChatEvent.java"));
|
||||
assertTrue(privateChat.contains("ModToolReportInputGuard.isValidPrivateChatLogCount(count)"),
|
||||
"private chat reports must reject negative or oversized client-provided chatlog counts");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportEventValidatesTopicBeforeUsingReply() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java"));
|
||||
|
||||
assertTrue(source.indexOf("if (cfhTopic == null)") < source.indexOf("cfhTopic.reply"),
|
||||
"ReportEvent must reject unknown topics before dereferencing the reply text");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bullyReportUsesSameMutedUserGateAsNormalReports() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java"));
|
||||
|
||||
assertTrue(source.contains("if (!this.client.getHabbo().getHabboStats().allowTalk())"),
|
||||
"bully reports must reject muted users instead of rejecting users who can talk");
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ModToolReportInputGuardTest {
|
||||
@Test
|
||||
void normalizesNullableMessages() {
|
||||
assertEquals("", ModToolReportInputGuard.normalize(null));
|
||||
assertEquals("report", ModToolReportInputGuard.normalize(" report "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportMessagesMustBeNonEmptyAndBounded() {
|
||||
assertFalse(ModToolReportInputGuard.isValidReportMessage(""));
|
||||
assertFalse(ModToolReportInputGuard.isValidReportMessage(null));
|
||||
assertTrue(ModToolReportInputGuard.isValidReportMessage("a".repeat(ModToolReportInputGuard.MAX_REPORT_MESSAGE_LENGTH)));
|
||||
assertFalse(ModToolReportInputGuard.isValidReportMessage("a".repeat(ModToolReportInputGuard.MAX_REPORT_MESSAGE_LENGTH + 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void privateChatLogCountsAreBounded() {
|
||||
assertFalse(ModToolReportInputGuard.isValidPrivateChatLogCount(0));
|
||||
assertTrue(ModToolReportInputGuard.isValidPrivateChatLogCount(ModToolReportInputGuard.MAX_PRIVATE_CHAT_LOGS));
|
||||
assertFalse(ModToolReportInputGuard.isValidPrivateChatLogCount(ModToolReportInputGuard.MAX_PRIVATE_CHAT_LOGS + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void idsMustBePositive() {
|
||||
assertFalse(ModToolReportInputGuard.isPositiveId(0));
|
||||
assertFalse(ModToolReportInputGuard.isPositiveId(-1));
|
||||
assertTrue(ModToolReportInputGuard.isPositiveId(1));
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ModToolTicketGuardTest {
|
||||
@Test
|
||||
void idsMustBePositive() {
|
||||
assertFalse(ModToolTicketGuard.isPositiveId(0));
|
||||
assertFalse(ModToolTicketGuard.isPositiveId(-1));
|
||||
assertTrue(ModToolTicketGuard.isPositiveId(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseBatchIsBounded() {
|
||||
assertFalse(ModToolTicketGuard.isValidReleaseBatch(0));
|
||||
assertTrue(ModToolTicketGuard.isValidReleaseBatch(ModToolTicketGuard.MAX_RELEASE_BATCH));
|
||||
assertFalse(ModToolTicketGuard.isValidReleaseBatch(ModToolTicketGuard.MAX_RELEASE_BATCH + 1));
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ModToolTicketLifecycleContractTest {
|
||||
@Test
|
||||
void mutatingTicketActionsValidateOwnership() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
"ModToolCloseTicketEvent.java",
|
||||
"ModToolIssueChangeTopicEvent.java",
|
||||
"ModToolReleaseTicketEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())"),
|
||||
handler + " must only mutate tickets owned by the acting moderator");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientDrivenTicketAndChatlogIdsAreValidated() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
"ModToolPickTicketEvent.java",
|
||||
"ModToolCloseTicketEvent.java",
|
||||
"ModToolIssueChangeTopicEvent.java",
|
||||
"ModToolRequestIssueChatlogEvent.java",
|
||||
"ModToolRequestRoomChatlogEvent.java",
|
||||
"ModToolRequestRoomUserChatlogEvent.java",
|
||||
"ModToolRequestUserChatlogEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("ModToolTicketGuard.isPositiveId"),
|
||||
handler + " must reject zero or negative client-provided ids");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseBatchAndCloseStateAreBounded() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
String release = Files.readString(base.resolve("ModToolReleaseTicketEvent.java"));
|
||||
String close = Files.readString(base.resolve("ModToolCloseTicketEvent.java"));
|
||||
|
||||
assertTrue(release.contains("ModToolTicketGuard.isValidReleaseBatch(count)"),
|
||||
"release ticket batches must be bounded before reading ticket ids");
|
||||
assertTrue(close.contains("state < 1 || state > 3"),
|
||||
"close ticket must reject unknown close states before mutating the ticket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeTopicRequiresKnownCategory() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java"));
|
||||
|
||||
assertTrue(source.contains("getCfhTopic(categoryId) == null"),
|
||||
"change-topic must reject unknown CFH categories before persisting");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# Earnings Center Packet Contract
|
||||
|
||||
This document is the emulator-side contract for the "Guadagni" UI.
|
||||
|
||||
## Incoming
|
||||
|
||||
### `RequestEarningsCenterEvent`
|
||||
|
||||
- Header: `9308`
|
||||
- Body: empty
|
||||
- Response: `EarningsCenterComposer`
|
||||
|
||||
### `ClaimEarningsRewardEvent`
|
||||
|
||||
- Header: `9309`
|
||||
- Body:
|
||||
- `String categoryKey`
|
||||
- Response: `EarningsClaimResultComposer`
|
||||
|
||||
### `ClaimAllEarningsRewardsEvent`
|
||||
|
||||
- Header: `9310`
|
||||
- Body: empty
|
||||
- Response: `EarningsClaimResultComposer`
|
||||
|
||||
## Outgoing
|
||||
|
||||
### `EarningsCenterComposer`
|
||||
|
||||
- Header: `9407`
|
||||
- Body:
|
||||
- `int entryCount`
|
||||
- repeated entry:
|
||||
- `String categoryKey`
|
||||
- `boolean enabled`
|
||||
- `boolean claimable`
|
||||
- `int nextClaimAt`
|
||||
- `int rewardCount`
|
||||
- repeated reward:
|
||||
- `String type`
|
||||
- `int amount`
|
||||
- `int pointsType`
|
||||
- `String data`
|
||||
|
||||
### `EarningsClaimResultComposer`
|
||||
|
||||
- Header: `9408`
|
||||
- Body:
|
||||
- `int resultCount`
|
||||
- repeated result:
|
||||
- `String categoryKey`
|
||||
- `String status`
|
||||
- `boolean success`
|
||||
- `boolean hasEntry`
|
||||
- entry body when `hasEntry=true`, same shape as `EarningsCenterComposer`
|
||||
|
||||
## Categories
|
||||
|
||||
- `daily_gift`
|
||||
- `games`
|
||||
- `achievements`
|
||||
- `marketplace`
|
||||
- `hc_payday`
|
||||
- `level_progress`
|
||||
- `donations`
|
||||
- `bonus_bag`
|
||||
- `mystery_boxes`
|
||||
- `club_job`
|
||||
|
||||
## Reward Types
|
||||
|
||||
- `credits`
|
||||
- `pixels`
|
||||
- `points`
|
||||
- `badge`
|
||||
- `item`
|
||||
- `hc_days`
|
||||
|
||||
For `points`, `pointsType` carries the currency type. For `badge`, `data` carries the badge code. For `item`, `data` carries the `items_base.id`. Other reward types keep `data` empty.
|
||||
|
||||
`marketplace` and `hc_payday` can be native rows. In native mode the amounts come from existing server state:
|
||||
|
||||
- `marketplace`: sold marketplace offers waiting for payout
|
||||
- `hc_payday`: unclaimed rows in `logs_hc_payday`
|
||||
- `achievements`: configured rewards gated by achievement score buckets
|
||||
- `level_progress`: configured rewards gated by citizenship/helper talent level
|
||||
|
||||
## Result Status
|
||||
|
||||
- `success`
|
||||
- `disabled`
|
||||
- `unknown_category`
|
||||
- `already_claimed`
|
||||
- `no_reward`
|
||||
- `error`
|
||||
|
||||
The client must not send reward amounts. Claim eligibility and rewards are always server authoritative.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Earnings Center Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build an emulator-owned earnings/rewards hub for the new "Guadagni" UI, with server-side reward definitions and claim protection.
|
||||
|
||||
**Architecture:** Add a focused earnings package under `com.eu.habbo.habbohotel.earnings`, wire three incoming handlers and two outgoing composers, and persist claims in a dedicated table with a unique period key. Keep reward definitions config-driven so UI/renderer work can progress independently.
|
||||
|
||||
**Tech Stack:** Java 21, Maven, MariaDB SQL updates, existing Arcturus packet manager/composer patterns, JUnit tests.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Map Existing Patterns
|
||||
|
||||
**Files:**
|
||||
- Read: `Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java`
|
||||
- Read: `Emulator/src/main/java/com/eu/habbo/messages/PacketNames.java`
|
||||
- Read: `Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java`
|
||||
- Read: `Emulator/src/main/java/com/eu/habbo/messages/outgoing/MessageComposer.java`
|
||||
|
||||
- [ ] Inspect packet registration and composer header lookup.
|
||||
- [ ] Inspect currency grant methods on `Habbo`.
|
||||
- [ ] Inspect emulator setting access APIs.
|
||||
- [ ] Choose the smallest implementation that matches existing style.
|
||||
|
||||
### Task 2: Add Earnings Domain
|
||||
|
||||
**Files:**
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/habbohotel/earnings/EarningsCategory.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/habbohotel/earnings/EarningsReward.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/habbohotel/earnings/EarningsEntry.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/habbohotel/earnings/EarningsClaimResult.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/habbohotel/earnings/EarningsCenterManager.java`
|
||||
|
||||
- [ ] Define allowlisted categories and client keys.
|
||||
- [ ] Load enabled flags, cooldowns, and reward values from configuration.
|
||||
- [ ] Build row state for a user.
|
||||
- [ ] Implement single claim and claim-all.
|
||||
- [ ] Grant credits/pixels/points through existing `Habbo` APIs.
|
||||
- [ ] Grant badges, furni items, and HC days through existing emulator storage paths.
|
||||
- [ ] Roll back a claim marker if a DB-backed grant fails.
|
||||
|
||||
### Task 3: Add Persistence
|
||||
|
||||
**Files:**
|
||||
- Create: `Database Updates/012_earnings_center.sql`
|
||||
|
||||
- [ ] Create `users_earnings_claims`.
|
||||
- [ ] Add unique key on `user_id`, `category`, `period_key`.
|
||||
- [ ] Keep the migration additive and safe for existing databases.
|
||||
|
||||
### Task 4: Add Packet Bridge
|
||||
|
||||
**Files:**
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/messages/incoming/earnings/RequestEarningsCenterEvent.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/messages/incoming/earnings/ClaimEarningsRewardEvent.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/messages/incoming/earnings/ClaimAllEarningsRewardsEvent.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/messages/outgoing/earnings/EarningsCenterComposer.java`
|
||||
- Create: `Emulator/src/main/java/com/eu/habbo/messages/outgoing/earnings/EarningsClaimResultComposer.java`
|
||||
- Modify: packet registration/mapping files discovered in Task 1.
|
||||
|
||||
- [ ] Incoming handlers parse only category keys.
|
||||
- [ ] Outgoing composers serialize rows and claim results.
|
||||
- [ ] Packet names are documented for renderer alignment.
|
||||
|
||||
### Task 5: Test and Build
|
||||
|
||||
**Files:**
|
||||
- Create: `Emulator/src/test/java/com/eu/habbo/habbohotel/earnings/EarningsCenterManagerTest.java`
|
||||
|
||||
- [ ] Test disabled feature behavior.
|
||||
- [ ] Test unknown category rejection.
|
||||
- [ ] Test single claim success.
|
||||
- [ ] Test duplicate claim rejection.
|
||||
- [ ] Test claim-all partial success.
|
||||
- [ ] Test badge, item, and HC reward serialization state.
|
||||
- [ ] Test claim rollback after grant failure.
|
||||
- [ ] Run focused tests.
|
||||
- [ ] Run `mvn clean package`.
|
||||
|
||||
### Task 6: Commit and PR
|
||||
|
||||
**Files:**
|
||||
- Commit all source, test, SQL, spec, and plan files.
|
||||
|
||||
- [ ] Commit spec and plan.
|
||||
- [ ] Commit implementation.
|
||||
- [ ] Push `feat/earnings-center` to `simoleo89/Arcturus-Morningstar-Extended`.
|
||||
- [ ] Open ready-for-review PR to `duckietm/Arcturus-Morningstar-Extended:dev`.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Earnings Center Design
|
||||
|
||||
## Goal
|
||||
|
||||
Add an emulator-owned rewards hub for the "Guadagni" UI. The client and renderer may decide how it looks, but the emulator must own reward amounts, claim eligibility, cooldowns, and anti-abuse checks.
|
||||
|
||||
## Scope
|
||||
|
||||
The first emulator version exposes ten earnings categories:
|
||||
|
||||
- `daily_gift`
|
||||
- `games`
|
||||
- `achievements`
|
||||
- `marketplace`
|
||||
- `hc_payday`
|
||||
- `level_progress`
|
||||
- `donations`
|
||||
- `bonus_bag`
|
||||
- `mystery_boxes`
|
||||
- `club_job`
|
||||
|
||||
Every category can be enabled, disabled, configured with one or more reward currencies, and claimed through a single-row claim or a claim-all request. Categories that are not yet backed by a native hotel subsystem still work through static configuration, so the UI contract is stable while deeper integrations are added later.
|
||||
|
||||
Supported configured reward types are credits, pixels/duckets, seasonal points, badges, furni items, and HC days.
|
||||
|
||||
## Architecture
|
||||
|
||||
Add a focused `com.eu.habbo.habbohotel.earnings` package:
|
||||
|
||||
- `EarningsCenterManager` loads category definitions from emulator settings, builds per-user state, and performs claims.
|
||||
- `EarningsCategory` is the allowlisted category enum and carries the client key.
|
||||
- `EarningsReward` represents one configured reward.
|
||||
- `EarningsEntry` is the serializable row state sent to the client.
|
||||
- `EarningsClaimResult` reports single/all claim outcomes.
|
||||
|
||||
The packet layer only parses category keys and delegates to the manager. The client never sends amounts, cooldowns, or reward definitions.
|
||||
|
||||
## Persistence
|
||||
|
||||
Add a database update that creates `users_earnings_claims`:
|
||||
|
||||
- `id`
|
||||
- `user_id`
|
||||
- `category`
|
||||
- `period_key`
|
||||
- `claimed_at`
|
||||
- unique key on `user_id`, `category`, `period_key`
|
||||
|
||||
The unique key is the main double-claim guard. `period_key` is calculated by the emulator from the category cooldown. Daily-style rewards use the UTC date key by default. One-time or long cooldown rows can use the cooldown bucket derived from `claimed_at`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add emulator settings with safe defaults:
|
||||
|
||||
- `earnings.enabled=0`
|
||||
- `earnings.<category>.enabled=1`
|
||||
- `earnings.<category>.cooldown.seconds=86400`
|
||||
- `earnings.<category>.credits=0`
|
||||
- `earnings.<category>.pixels=0`
|
||||
- `earnings.<category>.points=0`
|
||||
- `earnings.<category>.points.type=5`
|
||||
- `earnings.<category>.badge=`
|
||||
- `earnings.<category>.item_id=0`
|
||||
- `earnings.<category>.item.quantity=1`
|
||||
- `earnings.<category>.hc.days=0`
|
||||
- `earnings.<category>.native.enabled=0/1`
|
||||
|
||||
The feature defaults off so existing hotels do not receive surprise economy changes after deploying the jar.
|
||||
Marketplace and HC payday default to native integrations once the feature is enabled, because both already have server-side claim ledgers.
|
||||
Achievements and level progress use native eligibility by default: achievement score buckets and talent-track levels decide when the configured reward may be claimed.
|
||||
|
||||
## Packet Contract
|
||||
|
||||
Add three incoming handlers:
|
||||
|
||||
- `RequestEarningsCenterEvent`
|
||||
- `ClaimEarningsRewardEvent`
|
||||
- `ClaimAllEarningsRewardsEvent`
|
||||
|
||||
Add two outgoing composers:
|
||||
|
||||
- `EarningsCenterComposer`
|
||||
- `EarningsClaimResultComposer`
|
||||
|
||||
Composer format is intentionally simple and renderer-friendly: category key, enabled state, claimable state, next claim timestamp, rewards, and result code. Header IDs must be wired through `messages.ini`/packet registration in the same style as the rest of the emulator. If the renderer side chooses final IDs later, only the packet mapping should need adjustment.
|
||||
|
||||
## Security
|
||||
|
||||
- Reject unknown category keys.
|
||||
- Reject all claims when `earnings.enabled=0`.
|
||||
- Never trust reward amounts from the client.
|
||||
- Clamp configured rewards to non-negative values and bounded item/HC limits.
|
||||
- Roll back the claim record if a DB-backed reward grant fails.
|
||||
- Use the database unique key to prevent concurrent double claims.
|
||||
- `claim all` processes only claimable rows and returns per-category results.
|
||||
- Marketplace claims use the existing marketplace sold-offer payout path.
|
||||
- HC payday claims use existing unclaimed `logs_hc_payday` rows.
|
||||
- Achievement claims can be scoped to score buckets via `earnings.achievements.score.step`.
|
||||
- Level progress claims can be scoped to the current highest citizenship/helper level.
|
||||
|
||||
## Tests
|
||||
|
||||
Add unit tests around the manager-level logic:
|
||||
|
||||
- disabled global feature returns disabled rows and rejects claims
|
||||
- unknown category is rejected
|
||||
- successful claim grants configured currency once
|
||||
- duplicate claim in the same period is rejected
|
||||
- claim-all grants all claimable rows and skips already claimed rows
|
||||
- badge/item/HC reward rows are included in state
|
||||
- failed reward grants roll back the claim record
|
||||
|
||||
Packet tests can remain light because renderer IDs may be finalized separately; the critical behavior is the server-side claim guard.
|
||||
Reference in New Issue
Block a user