You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 15:36:17 +00:00
feat(earnings): add emulator rewards center
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
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.');
|
||||||
@@ -160,6 +160,13 @@ public final class Emulator {
|
|||||||
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||||
Emulator.config.register("gui.enabled", "0");
|
Emulator.config.register("gui.enabled", "0");
|
||||||
Emulator.config.register("gui.autostart.enabled", "0");
|
Emulator.config.register("gui.autostart.enabled", "0");
|
||||||
|
Emulator.config.register("rcon.rate_limit.enabled", "1");
|
||||||
|
Emulator.config.register("rcon.rate_limit.limit_for_period", "60");
|
||||||
|
Emulator.config.register("rcon.rate_limit.refresh_period_ms", "1000");
|
||||||
|
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());
|
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||||
System.out.println(startupCard(hotelTimezoneId));
|
System.out.println(startupCard(hotelTimezoneId));
|
||||||
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
|
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
|
||||||
@@ -482,6 +489,37 @@ public final class Emulator {
|
|||||||
return gameServer;
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static RCONServer getRconServer() {
|
public static RCONServer getRconServer() {
|
||||||
return rconServer;
|
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,347 @@
|
|||||||
|
package com.eu.habbo.habbohotel.earnings;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||||
|
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 Clock clock;
|
||||||
|
|
||||||
|
public EarningsCenterManager() {
|
||||||
|
this(new EmulatorConfigSource(), new JdbcClaimRepository(), new HabboRewardApplier(), Clock.systemUTC());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, Clock clock) {
|
||||||
|
this.config = config;
|
||||||
|
this.claims = claims;
|
||||||
|
this.rewards = rewards;
|
||||||
|
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(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(category);
|
||||||
|
|
||||||
|
if (!definition.enabled()) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.DISABLED, buildEntry(userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.rewards().isEmpty()) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
String periodKey = periodKey(now, definition.cooldownSeconds());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.claims.recordClaim(userId, category.getKey(), periodKey, now)) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.ALREADY_CLAIMED, buildEntry(userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rewards.grant(habbo, definition.rewards());
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(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(userId, category, now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsEntry buildEntry(int userId, EarningsCategory category, int now) {
|
||||||
|
CategoryDefinition definition = loadDefinition(category);
|
||||||
|
boolean claimable = false;
|
||||||
|
int nextClaimAt = 0;
|
||||||
|
|
||||||
|
if (definition.enabled() && !definition.rewards().isEmpty()) {
|
||||||
|
String periodKey = periodKey(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(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<>();
|
||||||
|
|
||||||
|
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 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(int now, int cooldownSeconds) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.crafting.*;
|
||||||
import com.eu.habbo.messages.incoming.events.calendar.AdventCalendarForceOpenEvent;
|
import com.eu.habbo.messages.incoming.events.calendar.AdventCalendarForceOpenEvent;
|
||||||
import com.eu.habbo.messages.incoming.events.calendar.AdventCalendarOpenDayEvent;
|
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.FloorPlanEditorRequestBlockedTilesEvent;
|
||||||
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorRequestDoorSettingsEvent;
|
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorRequestDoorSettingsEvent;
|
||||||
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorSaveEvent;
|
import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorSaveEvent;
|
||||||
@@ -130,6 +131,7 @@ public class PacketManager {
|
|||||||
this.registerCrafting();
|
this.registerCrafting();
|
||||||
this.registerCamera();
|
this.registerCamera();
|
||||||
this.registerGameCenter();
|
this.registerGameCenter();
|
||||||
|
this.registerEarnings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PacketNames getNames() {
|
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.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class);
|
||||||
this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.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 WheelAdminSavePrizesEvent = 9305;
|
||||||
public static final int SoundboardPlayEvent = 9306;
|
public static final int SoundboardPlayEvent = 9306;
|
||||||
public static final int SoundboardSetEnabledEvent = 9307;
|
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 RequestMentionsEvent = 4803;
|
||||||
public static final int MarkMentionsReadEvent = 4804;
|
public static final int MarkMentionsReadEvent = 4804;
|
||||||
public static final int DeleteMentionEvent = 4805;
|
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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -607,6 +607,8 @@ public class Outgoing {
|
|||||||
public static final int WheelAdminPrizesComposer = 9404;
|
public static final int WheelAdminPrizesComposer = 9404;
|
||||||
public static final int SoundboardSettingsComposer = 9405;
|
public static final int SoundboardSettingsComposer = 9405;
|
||||||
public static final int SoundboardPlayComposer = 9406;
|
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 MentionReceivedComposer = 4801;
|
||||||
public static final int MentionsListComposer = 4802;
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@
|
|||||||
- [ ] Build row state for a user.
|
- [ ] Build row state for a user.
|
||||||
- [ ] Implement single claim and claim-all.
|
- [ ] Implement single claim and claim-all.
|
||||||
- [ ] Grant credits/pixels/points through existing `Habbo` APIs.
|
- [ ] 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
|
### Task 3: Add Persistence
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@
|
|||||||
- [ ] Test single claim success.
|
- [ ] Test single claim success.
|
||||||
- [ ] Test duplicate claim rejection.
|
- [ ] Test duplicate claim rejection.
|
||||||
- [ ] Test claim-all partial success.
|
- [ ] Test claim-all partial success.
|
||||||
|
- [ ] Test badge, item, and HC reward serialization state.
|
||||||
|
- [ ] Test claim rollback after grant failure.
|
||||||
- [ ] Run focused tests.
|
- [ ] Run focused tests.
|
||||||
- [ ] Run `mvn clean package`.
|
- [ ] Run `mvn clean package`.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ The first emulator version exposes ten earnings categories:
|
|||||||
|
|
||||||
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.
|
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
|
## Architecture
|
||||||
|
|
||||||
Add a focused `com.eu.habbo.habbohotel.earnings` package:
|
Add a focused `com.eu.habbo.habbohotel.earnings` package:
|
||||||
@@ -57,6 +59,10 @@ Add emulator settings with safe defaults:
|
|||||||
- `earnings.<category>.pixels=0`
|
- `earnings.<category>.pixels=0`
|
||||||
- `earnings.<category>.points=0`
|
- `earnings.<category>.points=0`
|
||||||
- `earnings.<category>.points.type=5`
|
- `earnings.<category>.points.type=5`
|
||||||
|
- `earnings.<category>.badge=`
|
||||||
|
- `earnings.<category>.item_id=0`
|
||||||
|
- `earnings.<category>.item.quantity=1`
|
||||||
|
- `earnings.<category>.hc.days=0`
|
||||||
|
|
||||||
The feature defaults off so existing hotels do not receive surprise economy changes after deploying the jar.
|
The feature defaults off so existing hotels do not receive surprise economy changes after deploying the jar.
|
||||||
|
|
||||||
@@ -80,7 +86,8 @@ Composer format is intentionally simple and renderer-friendly: category key, ena
|
|||||||
- Reject unknown category keys.
|
- Reject unknown category keys.
|
||||||
- Reject all claims when `earnings.enabled=0`.
|
- Reject all claims when `earnings.enabled=0`.
|
||||||
- Never trust reward amounts from the client.
|
- Never trust reward amounts from the client.
|
||||||
- Clamp configured rewards to non-negative values.
|
- 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.
|
- Use the database unique key to prevent concurrent double claims.
|
||||||
- `claim all` processes only claimable rows and returns per-category results.
|
- `claim all` processes only claimable rows and returns per-category results.
|
||||||
|
|
||||||
@@ -93,5 +100,7 @@ Add unit tests around the manager-level logic:
|
|||||||
- successful claim grants configured currency once
|
- successful claim grants configured currency once
|
||||||
- duplicate claim in the same period is rejected
|
- duplicate claim in the same period is rejected
|
||||||
- claim-all grants all claimable rows and skips already claimed rows
|
- 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.
|
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