Compare commits

...

272 Commits

Author SHA1 Message Date
DuckieTM 38e7b6e880 Merge pull request #273 from simoleo89/fix/wired-dev-compile
fix(build): restore emulator package
2026-06-19 11:50:48 +02:00
simoleo89 d451a3dc71 fix(build): restore emulator package 2026-06-19 10:17:33 +02:00
DuckieTM caaff4f591 Merge pull request #272 from simoleo89/fix/room-rights-guild-membership
fix(room-rights): require guild membership
2026-06-18 12:56:15 +02:00
DuckieTM a89d18e8de Merge pull request #271 from simoleo89/fix/guilds-admin-removal
fix(guilds): protect admin members
2026-06-18 12:55:57 +02:00
DuckieTM 733cce25d7 Merge pull request #270 from simoleo89/fix/pets-package-name-inputs
fix(pets): bound package names
2026-06-18 12:55:27 +02:00
DuckieTM 9bc5ad5fb0 Merge pull request #269 from simoleo89/fix/pets-use-item-ownership
fix(pets): require item owner for pet consumables
2026-06-18 12:54:58 +02:00
DuckieTM ee9feb95fd Merge pull request #268 from simoleo89/fix/crafting-secret-ingredients
fix(crafting): bound secret ingredients
2026-06-18 12:54:33 +02:00
DuckieTM 0e650733df Merge pull request #267 from simoleo89/fix/commands-redeem-values
fix(commands): bound redeem values
2026-06-18 12:54:09 +02:00
DuckieTM 518957ddde Merge pull request #266 from simoleo89/fix/room-items-picked-owner
fix(room-items): restore picked item owner
2026-06-18 12:53:41 +02:00
DuckieTM 39168c27c8 Merge pull request #265 from simoleo89/fix/trade-ownership-batch-counts
fix(trade): verify ownership transfers
2026-06-18 12:53:16 +02:00
DuckieTM 8d31a4a81b Merge pull request #264 from simoleo89/fix/catalog-target-offer-limits
fix(catalog): bound target offer purchases
2026-06-18 12:52:52 +02:00
DuckieTM 4dac2b72bc Merge pull request #263 from simoleo89/fix/room-moderation-targets
fix(room-moderation): protect owners
2026-06-18 12:52:07 +02:00
DuckieTM a2fb6c820e Merge pull request #262 from simoleo89/fix/room-settings-inputs
fix(room-settings): bound inputs
2026-06-18 12:51:42 +02:00
DuckieTM d023d69524 Merge pull request #261 from simoleo89/fix/wired-reward-inputs
fix(wired): bound reward inputs
2026-06-18 12:51:12 +02:00
DuckieTM 365d3258f3 Merge pull request #260 from simoleo89/fix/wired-extra-selector-payloads
fix(wired): guard extra selector payloads
2026-06-18 12:50:46 +02:00
DuckieTM a91aac7538 Merge pull request #258 from simoleo89/fix/wired-movement-effect-payloads
fix(wired): bound movement effect payloads
2026-06-18 12:50:23 +02:00
DuckieTM fe57dee67f Merge branch 'dev' into fix/wired-movement-effect-payloads 2026-06-18 12:50:10 +02:00
DuckieTM 1775f0b222 Merge pull request #259 from simoleo89/fix/wired-utility-effect-payloads
fix(wired): bound utility effect payloads
2026-06-18 12:49:23 +02:00
DuckieTM 60a15c96a0 Merge pull request #257 from simoleo89/fix/wired-bot-effect-payloads
fix(wired): bound bot effect payloads
2026-06-18 12:49:02 +02:00
DuckieTM f2aaa2b472 Merge pull request #256 from simoleo89/fix/wired-trigger-payloads
fix(wired): bound trigger payloads
2026-06-18 12:48:39 +02:00
DuckieTM b839aec641 Merge branch 'dev' into fix/wired-trigger-payloads 2026-06-18 12:48:29 +02:00
DuckieTM 982fe35863 Merge pull request #254 from simoleo89/fix/wired-date-payloads
fix(wired): bound date payloads
2026-06-18 12:46:29 +02:00
DuckieTM 6b41b9dc47 Merge pull request #255 from simoleo89/fix/wired-match-payloads
fix(wired): bound match payloads
2026-06-18 12:45:53 +02:00
DuckieTM 673467e81c Merge branch 'dev' into fix/wired-match-payloads 2026-06-18 12:45:43 +02:00
DuckieTM 36772b6e6f Merge branch 'dev' into fix/wired-date-payloads 2026-06-18 12:44:59 +02:00
DuckieTM 70adcd7b18 Merge pull request #253 from simoleo89/fix/wired-variable-payloads
fix(wired): bound variable payloads
2026-06-18 12:44:20 +02:00
DuckieTM ee85888db3 Merge pull request #250 from simoleo89/fix/wired-furni-selection-payloads
fix(wired): bound furni selection payloads
2026-06-18 12:43:54 +02:00
DuckieTM fa942d3623 Merge branch 'dev' into fix/wired-furni-selection-payloads 2026-06-18 12:43:41 +02:00
DuckieTM 8102fbf053 Merge pull request #249 from simoleo89/fix/wired-avatar-condition-payloads
fix(wired): bound avatar condition payloads
2026-06-18 12:42:04 +02:00
DuckieTM 5ccef29944 Merge branch 'dev' into fix/wired-avatar-condition-payloads 2026-06-18 12:41:53 +02:00
DuckieTM a05f61703f Merge pull request #248 from simoleo89/fix/wired-count-time-payloads
fix(wired): bound count time payloads
2026-06-18 12:40:15 +02:00
DuckieTM 07bfc46e66 Merge branch 'dev' into fix/wired-count-time-payloads 2026-06-18 12:40:04 +02:00
DuckieTM f290a99704 Merge pull request #247 from simoleo89/fix/wired-user-condition-payloads
fix(wired): bound user condition payloads
2026-06-18 12:38:19 +02:00
DuckieTM ea7daf713c Merge pull request #246 from simoleo89/fix/wired-furni-condition-payloads
fix(wired): bound furni condition payloads
2026-06-18 12:37:51 +02:00
DuckieTM 2ac1cad96b Merge pull request #245 from simoleo89/fix/wired-team-condition-inputs
fix(wired): bound team condition inputs
2026-06-18 12:36:40 +02:00
DuckieTM 1116602c44 Merge pull request #244 from simoleo89/fix/wired-user-condition-inputs
fix(wired): bound user condition inputs
2026-06-18 12:36:11 +02:00
DuckieTM 867b146afd Merge pull request #243 from simoleo89/fix/wired-user-action-inputs
fix(wired): bound user action inputs
2026-06-18 12:35:35 +02:00
DuckieTM a6a4015211 Merge pull request #242 from simoleo89/fix/wired-date-time-inputs
fix(wired): bound date range inputs
2026-06-18 12:35:08 +02:00
DuckieTM 4db976e46e Merge pull request #241 from simoleo89/fix/wired-match-position-inputs
fix(wired): bound match position inputs
2026-06-18 12:34:44 +02:00
DuckieTM 3ee5c1079a Merge pull request #240 from simoleo89/fix/wired-furni-condition-inputs
fix(wired): bound furni condition inputs
2026-06-18 12:34:04 +02:00
DuckieTM d044ce1604 Merge pull request #239 from simoleo89/fix/wired-condition-inputs
fix(wired): bound condition inputs
2026-06-18 12:33:29 +02:00
DuckieTM 463ae925dd Merge pull request #238 from simoleo89/fix/wired-trigger-timers
fix(wired): clamp trigger timers
2026-06-18 12:33:01 +02:00
DuckieTM f25c2a8485 Merge pull request #237 from simoleo89/fix/wired-legacy-furni-parsing
fix(wired): tolerate legacy furni data
2026-06-18 12:32:32 +02:00
DuckieTM b88635a0a7 Merge pull request #236 from simoleo89/fix/wired-string-parsing
fix(wired): bound reward amounts
2026-06-18 12:31:59 +02:00
DuckieTM 9004538ec4 Merge pull request #235 from simoleo89/fix/wired-inputs
fix(wired): bound save payloads
2026-06-18 12:31:32 +02:00
simoleo89 804c2e639a fix(room-rights): require guild membership 2026-06-17 21:57:47 +02:00
simoleo89 4855ea6ea5 fix(guilds): protect admin members 2026-06-17 21:49:53 +02:00
simoleo89 c73eb49145 fix(pets): bound package names 2026-06-17 21:45:46 +02:00
simoleo89 ece9a214db fix(pets): require item owner for pet consumables 2026-06-17 21:42:28 +02:00
simoleo89 6b4da4f916 fix(crafting): bound secret ingredients 2026-06-17 21:39:52 +02:00
simoleo89 61b5cdee37 fix(commands): bound redeem values 2026-06-17 21:36:11 +02:00
simoleo89 4f2a5999e4 fix(room-items): restore picked item owner 2026-06-17 21:32:34 +02:00
simoleo89 7cf83cf158 fix(trade): verify ownership transfers 2026-06-17 21:26:04 +02:00
simoleo89 4ceac0bbc6 fix(catalog): bound target offer purchases 2026-06-17 21:22:03 +02:00
simoleo89 5768cc3f01 fix(room-moderation): protect owners 2026-06-17 21:15:37 +02:00
simoleo89 18d90a635e fix(room-settings): bound inputs 2026-06-17 21:12:06 +02:00
simoleo89 a90a8754bc fix(wired): bound reward inputs 2026-06-17 20:45:51 +02:00
simoleo89 305d1ca097 fix(wired): guard extra selector payloads 2026-06-17 20:38:40 +02:00
simoleo89 f292426529 fix(wired): bound utility effect payloads 2026-06-17 20:33:52 +02:00
simoleo89 a2d75ea487 fix(wired): bound movement effect payloads 2026-06-17 20:28:11 +02:00
simoleo89 60b998f909 fix(wired): bound bot effect payloads 2026-06-17 20:21:36 +02:00
simoleo89 fbe9eddb37 fix(wired): bound trigger payloads 2026-06-17 20:13:54 +02:00
simoleo89 80c8467f82 fix(wired): bound match payloads 2026-06-17 20:06:34 +02:00
simoleo89 23e6d4800a fix(wired): bound date payloads 2026-06-17 20:00:12 +02:00
simoleo89 c83cd22fff fix(wired): bound variable payloads 2026-06-17 19:55:04 +02:00
simoleo89 29ed72fca9 fix(wired): bound furni selection payloads 2026-06-17 19:25:11 +02:00
simoleo89 25e38fbbeb fix(wired): bound avatar condition payloads 2026-06-17 19:21:54 +02:00
simoleo89 60eeaca689 fix(wired): bound count time payloads 2026-06-17 19:18:23 +02:00
simoleo89 dc6a912632 fix(wired): bound user condition payloads 2026-06-17 19:13:00 +02:00
simoleo89 a2c958684c fix(wired): bound furni condition payloads 2026-06-17 19:09:52 +02:00
simoleo89 237c3a3425 fix(wired): bound team condition inputs 2026-06-17 19:04:56 +02:00
simoleo89 3304edafb7 fix(wired): bound user condition inputs 2026-06-17 18:54:27 +02:00
simoleo89 cf307e44da fix(wired): bound user action inputs 2026-06-17 18:50:06 +02:00
simoleo89 5dfa8df5f4 fix(wired): bound date range inputs 2026-06-17 18:40:48 +02:00
simoleo89 a5dabd924e fix(wired): bound match position inputs 2026-06-17 18:37:25 +02:00
simoleo89 4479763f12 fix(wired): bound furni condition inputs 2026-06-17 18:32:24 +02:00
simoleo89 7869938d98 fix(wired): bound condition inputs 2026-06-17 18:27:36 +02:00
simoleo89 916b9df7a3 fix(wired): clamp trigger timers 2026-06-17 18:20:55 +02:00
simoleo89 0ceeb5ca70 fix(wired): tolerate legacy furni data 2026-06-17 18:15:08 +02:00
simoleo89 2f46f31684 fix(wired): bound reward amounts 2026-06-17 18:11:08 +02:00
simoleo89 240fede12a fix(wired): bound save payloads 2026-06-17 18:06:36 +02:00
DuckieTM 0109c25c80 Merge pull request #234 from simoleo89/fix/items-data-lookups
fix(items): harden item data lookups
2026-06-17 10:03:23 +02:00
DuckieTM 5293b7fd5d Merge pull request #233 from simoleo89/fix/permissions-inputs
fix(permissions): fail closed on stale ranks
2026-06-17 10:03:05 +02:00
DuckieTM b5891ae7da Merge pull request #232 from simoleo89/fix/gameclients-inputs
fix(gameclients): bound login session inputs
2026-06-17 10:02:42 +02:00
DuckieTM b580c67dcf Merge branch 'dev' into fix/gameclients-inputs 2026-06-17 10:02:30 +02:00
DuckieTM 18a37fa92e Merge pull request #231 from simoleo89/fix/navigator-inputs
fix(navigator): bound search inputs
2026-06-17 10:01:55 +02:00
DuckieTM b846f066e4 Merge pull request #230 from simoleo89/fix/catalog-inputs
fix(catalog): bound marketplace inputs
2026-06-17 10:01:29 +02:00
DuckieTM 65cb89dc84 Merge pull request #229 from simoleo89/fix/commands-inputs
fix(commands): enforce staff target ceilings
2026-06-17 10:01:06 +02:00
DuckieTM f2a010b25c Merge pull request #228 from simoleo89/fix/modtool-inputs
fix(modtool): bound staff supplied targets
2026-06-17 10:00:31 +02:00
DuckieTM 42708c0b07 Merge pull request #227 from simoleo89/fix/room-item-inputs
fix(rooms): bound room item inputs
2026-06-17 10:00:11 +02:00
DuckieTM 12753dad2b Merge pull request #226 from simoleo89/fix/room-user-inputs
fix(rooms): bound room user inputs
2026-06-17 09:59:54 +02:00
DuckieTM c094f440b2 Merge pull request #225 from simoleo89/fix/user-profile-inputs
fix(users): bound profile setting inputs
2026-06-17 09:58:55 +02:00
DuckieTM b9d5fa2557 Merge pull request #224 from simoleo89/fix/guild-forum-inputs
fix(forums): bound guild forum view inputs
2026-06-17 09:57:06 +02:00
DuckieTM 4d4c796ad5 Merge pull request #223 from simoleo89/fix/guild-badge-guards
fix(guilds): bound guild management inputs
2026-06-17 09:56:28 +02:00
DuckieTM cc8efb7382 Merge pull request #222 from simoleo89/fix/polls-answer-guards
fix(polls): bound answer payloads
2026-06-17 09:56:03 +02:00
DuckieTM 272aab8309 Merge pull request #221 from simoleo89/fix/catalog-search-results
fix(catalog): repair search offer ids
2026-06-17 09:55:26 +02:00
DuckieTM f9fbbb0355 Merge pull request #220 from simoleo89/fix/room-settings-guards
fix(rooms): validate room settings inputs
2026-06-17 09:54:41 +02:00
DuckieTM 2dac462f81 Merge pull request #219 from simoleo89/fix/trading-packet-guards
fix(trading): bound offered item batches
2026-06-17 09:54:18 +02:00
DuckieTM 159ce25a88 Merge pull request #218 from simoleo89/fix/incoming-packet-guards
fix(friends): bound messenger inputs
2026-06-17 09:53:56 +02:00
DuckieTM 2ef2c59c80 Merge pull request #217 from simoleo89/fix/nitro-secure-asset-safety
fix(auth): bound secure asset file reads
2026-06-17 09:53:08 +02:00
DuckieTM 0a9753a5c0 Merge branch 'dev' into fix/nitro-secure-asset-safety 2026-06-17 09:52:06 +02:00
DuckieTM 40ae702e51 Merge pull request #216 from simoleo89/fix/nitro-secure-api-safety
fix(auth): bound secure api payloads
2026-06-17 09:45:55 +02:00
DuckieTM 1af212191e Merge branch 'dev' into fix/nitro-secure-api-safety 2026-06-17 09:45:42 +02:00
DuckieTM 7d0aacc490 Merge pull request #215 from simoleo89/fix/network-rcon-safety
fix(rcon): bound inbound payload handling
2026-06-17 09:44:38 +02:00
DuckieTM c75058d4e8 Merge pull request #214 from simoleo89/fix/auth-session-safety
fix(auth): bound session token inputs
2026-06-17 09:44:12 +02:00
simoleo89 1d7c5b856f fix(items): harden item data lookups 2026-06-16 21:53:39 +02:00
simoleo89 743cad8361 fix(permissions): fail closed on stale ranks 2026-06-16 21:48:45 +02:00
simoleo89 a37de4556b fix(gameclients): bound login session inputs 2026-06-16 21:44:10 +02:00
simoleo89 62454671d2 fix(navigator): bound search inputs 2026-06-16 21:36:06 +02:00
simoleo89 efe7897fb4 fix(catalog): bound marketplace inputs 2026-06-16 21:30:37 +02:00
simoleo89 032003b64c fix(commands): enforce staff target ceilings 2026-06-16 21:25:55 +02:00
simoleo89 e24020e9df fix(modtool): bound staff supplied targets 2026-06-16 21:17:27 +02:00
simoleo89 ba80870df0 fix(rooms): bound room item inputs 2026-06-16 21:10:38 +02:00
simoleo89 3342b22a76 fix(rooms): bound room user inputs 2026-06-16 20:57:11 +02:00
simoleo89 5b2c9f0aee fix(users): bound profile setting inputs 2026-06-16 20:47:58 +02:00
simoleo89 26f86e3e31 fix(friends): bound messenger inputs 2026-06-16 20:42:45 +02:00
simoleo89 9b9902c76d fix(forums): bound guild forum view inputs 2026-06-16 20:37:48 +02:00
simoleo89 112796e133 fix(guilds): bound guild management inputs 2026-06-16 20:34:27 +02:00
simoleo89 736b7c70b4 fix(polls): bound answer payloads 2026-06-16 20:26:19 +02:00
simoleo89 b0d4317c2d fix(catalog): repair search offer ids 2026-06-16 20:21:54 +02:00
simoleo89 4cf0af79d1 fix(rooms): validate room settings inputs 2026-06-16 20:16:33 +02:00
simoleo89 547c5ef157 fix(auth): bound secure api payloads 2026-06-16 20:08:42 +02:00
simoleo89 a433e5539d fix(rcon): bound inbound payload handling 2026-06-16 20:06:32 +02:00
simoleo89 b600ac499c fix(trading): bound offered item batches 2026-06-16 20:04:03 +02:00
simoleo89 1598297a2a fix(auth): bound secure asset file reads 2026-06-15 22:41:13 +02:00
simoleo89 37ce71ad1e fix(auth): bound session token inputs 2026-06-15 22:19:29 +02:00
DuckieTM 416d0bb088 Merge pull request #213 from simoleo89/fix/room-user-safety
Guard room user moderation packets
2026-06-15 22:18:09 +02:00
DuckieTM 9c3d887447 Merge pull request #212 from simoleo89/fix/room-item-safety
Harden room item packet guards
2026-06-15 22:17:39 +02:00
DuckieTM 316613db6e Merge pull request #211 from simoleo89/fix/catalog-inventory-safety
Harden catalog inventory safety guards
2026-06-15 22:17:14 +02:00
DuckieTM 5f4e91133e Merge branch 'dev' into fix/catalog-inventory-safety 2026-06-15 22:17:00 +02:00
DuckieTM 47dcbae4b3 Merge pull request #210 from simoleo89/feat/earnings-center
feat: add emulator earnings center
2026-06-15 22:16:09 +02:00
DuckieTM cdc0620c9b Merge pull request #209 from simoleo89/fix/forum-input-guards
fix(forums): validate guild forum inputs
2026-06-15 22:15:49 +02:00
simoleo89 827b130ccc fix(rooms): guard room user moderation packets 2026-06-15 22:15:39 +02:00
DuckieTM b7f153f8e7 Merge pull request #206 from simoleo89/fix/modtool-staff-action-guards
fix(modtool): harden staff and report workflows
2026-06-15 22:15:14 +02:00
simoleo89 bea385afe2 fix(rooms): harden room item packet guards 2026-06-15 22:07:24 +02:00
simoleo89 8c7d6db135 fix(catalog): harden marketplace and inventory mutations 2026-06-15 22:01:38 +02:00
simoleo89 95bd84a95f fix(rcon): register guard defaults before startup 2026-06-15 22:01:25 +02:00
simoleo89 22b05b4e52 feat(earnings): gate rewards by user progress 2026-06-15 21:49:45 +02:00
simoleo89 766d8d67d3 feat(earnings): integrate native reward sources 2026-06-15 21:14:35 +02:00
simoleo89 bd9657cf63 docs(earnings): document renderer packet contract 2026-06-15 20:48:43 +02:00
simoleo89 e29e06201c feat(earnings): add emulator rewards center 2026-06-15 20:41:00 +02:00
simoleo89 dac83e8a62 docs(earnings): define emulator rewards center 2026-06-15 20:25:48 +02:00
simoleo89 916ef7af3a fix(modtool): guard ticket lifecycle inputs 2026-06-15 20:15:47 +02:00
simoleo89 044d1141cd fix(modtool): validate report payloads 2026-06-15 20:15:46 +02:00
simoleo89 c98261d8c3 fix(forums): validate guild forum inputs 2026-06-15 20:13:42 +02:00
simoleo89 8ba9132e7e fix(modtool): bound staff supplied messages 2026-06-15 19:54:34 +02:00
simoleo89 36a06647f0 fix(modtool): enforce staff target rank ceilings 2026-06-15 19:51:36 +02:00
DuckieTM c48e01cb8e Merge pull request #205 from Lorenzune/pr-emulator-release-dispatch
Allow manual emulator release workflow
2026-06-15 07:25:13 +02:00
DuckieTM 916cf9ba9e Merge pull request #203 from simoleo89/fix/housekeeping-core-peer-rank
fix(housekeeping): harden privileged staff actions
2026-06-15 07:24:55 +02:00
DuckieTM 0af489cef2 Merge pull request #199 from simoleo89/fix/modtool-sanction-rank-ceilings
fix(modtool): enforce permissions and sanction rank ceilings
2026-06-15 07:24:30 +02:00
DuckieTM 6171ec7bab Merge pull request #198 from simoleo89/chore/deps-resilience-validation
fix(rcon): harden privileged commands and payloads
2026-06-15 07:24:12 +02:00
DuckieTM c048713b22 Merge branch 'dev' into chore/deps-resilience-validation 2026-06-15 07:24:02 +02:00
DuckieTM e5e3918513 Merge pull request #190 from simoleo89/fix/catalog-page-mutation-guards
fix(catalog): harden admin mutations and voucher claims
2026-06-15 07:22:47 +02:00
DuckieTM 14593b4638 Merge pull request #188 from simoleo89/fix/furnieditor-update-validation
fix(furni-editor): validate and sync furnidata changes
2026-06-15 07:22:24 +02:00
DuckieTM c199d805d8 Merge pull request #184 from simoleo89/fix/guild-badge-packet-parts
fix(guilds): validate badge packets and memberships
2026-06-15 07:22:01 +02:00
DuckieTM 3282430b67 Merge pull request #183 from simoleo89/fix/command-description-texts
fix(commands): complete and quiet command descriptions
2026-06-15 07:21:39 +02:00
DuckieTM 560def21d7 Merge pull request #180 from simoleo89/fix/items-ownership-and-charges
fix(items): harden ownership and redeem lifecycle
2026-06-15 07:21:09 +02:00
DuckieTM 5011fdf848 Merge pull request #179 from simoleo89/fix/rooms-self-moderation-scope
fix(rooms): scope room actions and bound rights removal
2026-06-15 07:20:41 +02:00
DuckieTM d34b44a656 Merge pull request #177 from simoleo89/style/startup-console
style(startup): console banner/splash/colors
2026-06-15 07:20:23 +02:00
DuckieTM 848b8bd5ce Merge pull request #176 from simoleo89/fix/messages-duplicate-aliases
fix(messages): silence duplicate packet aliases
2026-06-15 07:19:39 +02:00
DuckieTM 80400f828c Merge pull request #172 from simoleo89/fix/marketplace-claimed-payout
fix(marketplace): only pay out claimed offers after detach
2026-06-15 07:19:10 +02:00
DuckieTM 6868dd8d3d Merge pull request #171 from simoleo89/fix/trading-persistence-abort
fix(trading): harden trade lifecycle
2026-06-15 07:18:51 +02:00
Lorenzune 9f1e036310 Allow manual emulator release workflow 2026-06-15 02:16:23 +02:00
simoleo89 ec24283e0f fix(housekeeping): protect room owner mutations 2026-06-14 22:17:47 +02:00
simoleo89 93c4565660 fix(housekeeping): bound staff supplied text 2026-06-14 22:14:41 +02:00
simoleo89 31027095ec fix(housekeeping): enforce rank ceilings on rank changes 2026-06-14 21:55:19 +02:00
simoleo89 aa6dcd1062 fix(rcon): bound alert payloads 2026-06-14 21:40:59 +02:00
simoleo89 11554eae7b fix(rcon): validate social and room commands 2026-06-14 21:23:21 +02:00
simoleo89 25273679a1 fix(rcon): constrain remote command execution 2026-06-14 21:18:28 +02:00
simoleo89 15b56f9519 fix(rcon): bound mute and achievement mutations 2026-06-14 21:13:24 +02:00
simoleo89 8412a51ec4 fix(rcon): guard user update mutations 2026-06-14 21:02:28 +02:00
simoleo89 5d8dc670bd fix(rcon): cap subscription duration changes 2026-06-14 21:02:28 +02:00
simoleo89 81c8dfc605 fix(rcon): harden gift creation requests 2026-06-14 21:02:27 +02:00
simoleo89 4747699656 fix(rcon): validate room ownership and clothing grants 2026-06-14 21:02:27 +02:00
simoleo89 dba0337a7b fix(rcon): validate grant requests 2026-06-14 21:02:18 +02:00
simoleo89 3cb24a5185 fix(rcon): constrain setrank requests 2026-06-14 21:01:27 +02:00
simoleo89 775197984f fix(rcon): validate offline badge targets
GiveBadge could treat a missing offline user as eligible for a badge and insert through a nullable user subquery. Depending on SQL mode this could fail late or persist an orphaned user_id value. Resolve the offline user first, return HABBO_NOT_FOUND when absent, and insert badges with the resolved user id only.
2026-06-14 21:01:27 +02:00
simoleo89 4eafb54c57 fix(rcon): allow online motto updates outside rooms
SetMotto updated the in-memory motto and then unconditionally broadcast RoomUserData through the current room. Online users without a current room could throw a null-pointer exception after the state change, making the RCON call report an error despite mutating the user. Only broadcast room data when a room is present and cover the invariant with a contract test.
2026-06-14 21:01:26 +02:00
simoleo89 d8260ec461 fix(rcon): bind offline respect counters correctly
GiveRespect inverted the offline SQL parameters for respects_given and respects_received. Online users received the intended counters, but offline users had the two persisted counters swapped. Bind respect_given to respects_given and respect_received to respects_received, with a contract test to keep the RCON offline path aligned.
2026-06-14 21:01:26 +02:00
simoleo89 b94acdf719 fix(rcon): report missing offline credit targets
GiveCredits treated offline UPDATE execution as success without checking whether any user row was changed. Nonexistent user ids could therefore return an offline success response while granting nothing. Use executeUpdate(), return HABBO_NOT_FOUND when no row is affected, and keep SQL errors from falling through to the offline success message.
2026-06-14 21:01:26 +02:00
simoleo89 4330bf5a62 fix(rcon): always release inbound buffers
RCONServerHandler released the inbound ByteBuf only after successfully parsing, writing, flushing, and closing the response. Any exception before the tail release could leak Netty buffers and let malformed RCON traffic consume memory over time. Guard non-ByteBuf messages, release accepted buffers from a finally block, and add a contract test for the release invariant.
2026-06-14 21:01:16 +02:00
simoleo89 aaad94f954 fix(rcon): upsert offline pixel grants
RCON GivePixels previously used an UPDATE for offline users, so users without an existing users_currency type 0 row received no pixels while the command still returned success. Match the GivePoints and housekeeping paths with an upsert and add a contract test that keeps offline pixel grants creating missing currency rows.
2026-06-14 21:00:49 +02:00
simoleo89 d9cf70910f fix(housekeeping): cap sanction durations safely 2026-06-14 21:00:37 +02:00
simoleo89 fe0ba3b9e9 fix(housekeeping): validate grant mutations 2026-06-14 20:59:51 +02:00
simoleo89 4b81997e62 test(housekeeping): cover rank and currency audit logs
Rank changes and manual currency grants are among the highest-risk housekeeping actions. They already write audit entries, but the coverage contract did not list them, so a future regression could silently remove those logs. Extend the contract test to require audit logging for credit grants, currency grants, and rank changes.
2026-06-14 20:59:15 +02:00
simoleo89 79d734ef26 fix(housekeeping): audit room and session actions
The first audit coverage pass covered economy/account-impacting HK actions, but room and session mutators still returned success without an audit row. Add audit entries for room deletion, force disconnect, room kicks, user kicks, room mute, room state changes, and successful unbans, and extend the coverage contract to keep these privileged actions tracked.
2026-06-14 20:59:15 +02:00
simoleo89 dbcf139a52 fix(housekeeping): audit sensitive actions
Several privileged housekeeping handlers returned success without appending an audit entry, so the action log stayed incomplete even after the log table schema was fixed. Add audit writes for ban, mute, password reset, HC changes, trade lock, item grants, room ownership transfer, and hotel alerts, and cover the expected logging surface with a contract test.
2026-06-14 20:59:14 +02:00
simoleo89 98aab95d58 fix(housekeeping): align audit log schema
Housekeeping audit writes used an obsolete housekeeping_log schema with operator_id, operator_name, target_user_id and ip columns, while the migration and list composer read actor_id, actor_name, target_type, target_id, target_label, action, detail and success. That made log inserts fail against migrated databases and made auto-created tables unreadable by the client. Align the writer and auto-create DDL with the action-log schema, preserve operator IP in detail, and add a contract test for schema drift.
2026-06-14 20:59:14 +02:00
simoleo89 fb85952e88 fix(modtool): require support permission for kicks
ModToolKickEvent was the only staff-only modtool handler that called the moderation kick path without checking ACC_SUPPORTTOOL first. Gate it with the same support-tool permission and scripter handling used by the neighboring moderation actions, and add a contract test that keeps all staff-only modtool handlers behind ACC_SUPPORTTOOL.
2026-06-14 20:59:00 +02:00
simoleo89 54ef2ee251 docs(furni-editor): design spec — create furnidata entry if missing (upsert) 2026-06-14 20:58:23 +02:00
simoleo89 df2a849adc fix(rooms): bound rights removal batches 2026-06-14 20:58:23 +02:00
simoleo89 8e21765676 fix(polls): scope answers to active room poll
Require poll answer, cancel, and question-data packets to match the poll configured on the caller's current room. Previously a crafted packet could target any loaded poll id and submit the final question directly, including badge-reward polls, without being in a room where that poll was active.

Keep word quiz handling null-safe and add a contract test covering current-room poll scoping for all poll handlers.
2026-06-14 20:58:23 +02:00
simoleo89 0081280328 fix(catalog): claim vouchers before rewards
Move voucher exhaustion checks and history persistence behind a synchronized per-voucher claim path. Rewards are now applied only after the history row is inserted successfully, preventing duplicate or failed-claim redemption from granting credits, points, or catalog items.

Adds a contract test for claim ordering. Maven verification was attempted but blocked by sandbox network/plugin resolution after escalation usage was exhausted; diff --check passes.
2026-06-14 20:56:37 +02:00
simoleo89 2bc4340ec9 feat(furni-editor): create furnidata entry when missing (upsert on 10046)
FurniEditorUpdateFurnidataEvent (10046) was edit-only: FurnidataWriter.write()
refuses classnames absent from furnidata, so a furni with no entry showed the
DB-fallback name with locked fields and "Classname not found". Make it an upsert:

- FurnidataWriter.create(): append a complete entry (JSON5-preserving, atomic +
  backup) into the matching roomitemtypes/wallitemtypes furnitype array; guards
  against duplicate classname (ALREADY_EXISTS) and id collision (ID_COLLISION);
  split-tier writes to items.furnidata.create_tier (default "custom", file
  created with a shell if absent), single-file writes to the source.
- FurnidataEntryBuilder: build the complete entry from the item's items_base row
  (id = sprite id, classname, type-driven section, xdim/ydim, canstandon/
  cansiton/canlayon, name/desc, sane defaults matching existing entries).
- Handler: on write()==false, load the Item, build + create the entry, map
  CreateResult to a precise message; then the existing reindex + 10047 broadcast
  + public_name mirror run for both paths; audit action is "create" vs "edit".

No renderer change, no new packet. Pairs with the client unlocking name/desc when
the entry is missing (separate Nitro-V3 change).
2026-06-14 20:56:37 +02:00
simoleo89 93e5ea15aa docs(furni-editor): implementation plan — create furnidata entry if missing 2026-06-14 20:56:37 +02:00
simoleo89 aec61064ae fix(furnidata): prefer renderer config source
Resolve furnidata from the renderer config and asset base before falling back to the legacy items.furnidata.path override. This keeps the emulator aligned with the same furnidata URL the UI/renderer already consume.

Keep the legacy path as a compatibility fallback for older installs, but stop exposing absolute furnidata file paths in the startup log. The provider now reports a compact manager-style source label instead.

Add coverage proving renderer-config furnidata.url wins over the legacy path when both are present.
2026-06-14 20:56:37 +02:00
simoleo89 8db6281cc8 fix(guilds): only accept pending memberships
Guard the guild acceptance update with level_id = REQUESTED so a stale or concurrent accept cannot promote a membership row that has already changed state.

Tests: mvn '-Dtest=GuildManagerMembershipContractTest,GuildMembershipManagementContractTest,GuildMembershipRequestContractTest' test
2026-06-14 20:56:36 +02:00
simoleo89 8672c2d0ea fix(catalog): validate admin offer payloads 2026-06-14 20:56:36 +02:00
simoleo89 a92feb2ef0 fix(commands): quiet optional descriptions 2026-06-14 20:56:35 +02:00
simoleo89 478c4c70b8 fix(trading): prevent duplicate active trades
Guard RoomTradeManager.startTrade while holding the activeTrades lock so concurrent trade starts cannot register the same participant in multiple active trades before room status updates settle.

Add a contract test covering the lock-scoped participant guard and keep the existing trade safety tests green.
2026-06-14 20:56:34 +02:00
simoleo89 7ba0029ba8 fix(bots): preserve owner on pickup
Room owners can remove bots from their room, but picking up another user's bot must return it to the original owner instead of transferring ownership to the picker.

Tests: mvn -Dtest=BotPickupOwnershipContractTest test; mvn -DskipTests package
2026-06-14 20:51:19 +02:00
simoleo89 39c6e24097 fix(items): persist clothing grants before redeem
Redeeming clothing furniture now inserts the wardrobe grant before removing/deleting the voucher furniture. If the DB insert fails, the item remains in the room and the in-memory wardrobe is not updated.

Tests: mvn -Dtest=RedeemClothingContractTest test; mvn -DskipTests package
2026-06-14 20:51:19 +02:00
simoleo89 2b18ca2deb fix(housekeeping): allow core rank peer actions
Keep the housekeeping rank ceiling for normal staff, but treat the highest configured rank as the core rank so rank 7 can act on other rank 7 users without opening peer actions for lower staff ranks.

Tests: mvn '-Dtest=HousekeepingTargetRankGuardContractTest,HousekeepingMutationGuardTest,HousekeepingSetUserRankEventTest,HousekeepingTargetRankGuardContractTest' test
2026-06-14 20:24:51 +02:00
simoleo89 9ac50600f6 fix(housekeeping): enforce target rank ceiling 2026-06-14 20:24:50 +02:00
simoleo89 edddc551c5 fix(modtool): enforce sanction rank ceilings 2026-06-14 19:17:27 +02:00
simoleo89 1a03b8f3a9 fix(gui): require explicit dashboard autostart 2026-06-14 19:01:40 +02:00
simoleo89 d7fa02a453 fix(rcon): validate privileged payloads 2026-06-14 18:42:52 +02:00
simoleo89 994d539caf fix(rcon): rate limit remote command bursts 2026-06-14 18:31:58 +02:00
simoleo89 c6e43c6d55 fix(config): keep gui disabled by default 2026-06-14 18:18:20 +02:00
simoleo89 61972dafa4 fix(config): register gui enabled default 2026-06-14 18:15:29 +02:00
simoleo89 14a590235c fix(console): install jansi for forced ansi startup 2026-06-14 18:15:29 +02:00
simoleo89 39d21daeff chore(deps): add resilience and validation libraries 2026-06-14 17:56:20 +02:00
simoleo89 c9214bac07 fix(catalog): guard page mutations 2026-06-14 16:40:57 +02:00
simoleo89 fdcd3a7323 fix(furnieditor): validate item update payloads 2026-06-14 16:23:59 +02:00
simoleo89 7a7e38311d fix(guilds): validate badge packet parts 2026-06-14 15:51:43 +02:00
simoleo89 4359650621 fix(texts): add missing command descriptions 2026-06-14 15:51:10 +02:00
simoleo89 82c6f3f9ff fix(items): charge rentable space purchases
Deduct the computed rent cost when a user rents an InteractionRentableSpace. The previous flow only checked that the user had enough credits, then marked the space as rented without charging them, allowing free weekly rentals.

Honor ACC_INFINITE_CREDITS for staff accounts and add a contract test that keeps the charge before the rented state is assigned.
2026-06-13 18:24:16 +02:00
simoleo89 60ccc8c80b fix(items): require seed ownership for monsterplants
Reject monsterplant seed redemption when the caller does not own the placed seed. Without this guard, a user in the same room could trigger ToggleFloorItemEvent against another user's seed and have the server delete that item while creating the monsterplant pet for the attacker.

Add a contract test covering the ownership guard before createMonsterplant is reached.
2026-06-13 18:24:16 +02:00
simoleo89 eb41e3afb9 fix(rooms): scope self moderation to current room
Reject client-supplied room ids for self-moderation packets unless they match the caller's current room. This prevents users with saved rights or ownership in another room from muting, banning, or unbanning users remotely via crafted packets.

RoomUserBanEvent now also ignores invalid ban type values instead of letting valueOf throw through the message handler.

Add a contract test covering ban, mute, and unban current-room scoping.
2026-06-13 18:24:11 +02:00
simoleo89 a8e0534634 style(logging): colorize adaptive console logs
Route console log level and logger columns through custom Logback converters so terminals with ANSI support get colored severity badges and compact colored class names.

Keep the same habbo.console.style auto/ansi/plain behavior as the startup splash, including plain fallback for non-interactive output, NO_COLOR, and legacy Windows console paths.

The file appenders keep their existing verbose patterns unchanged, so debug/error log files remain plain and grep-friendly.

Cover the level formatter, logger formatter, override behavior, and Logback pattern wiring with tests.
2026-06-13 18:24:02 +02:00
simoleo89 98e366dd07 style(startup): add adaptive console colors
Add an auto-detected styled startup splash for terminals that support ANSI colors, including Windows Terminal, ANSICON, ConEmu ANSI, and common TERM-based consoles.

Keep the default and redirected-output path plain text so legacy CMD, logs, and service wrappers remain readable. The style can also be forced with -Dhabbo.console.style=ansi or disabled with -Dhabbo.console.style=plain.

Cover the styled splash, Windows Terminal detection, non-interactive fallback, and forced plain mode with startup console tests.
2026-06-13 18:24:02 +02:00
simoleo89 9edb984f56 style(startup): improve universal console layout
Keep the Morningstar ASCII logo while using a structured plain-text startup card that works in CMD, Windows Terminal, and other consoles without ANSI support.

Compact the Logback console pattern to use simple class names, clean separators, and a wider message column so startup logs do not wrap as aggressively. Simplify Infostand startup output to a one-line asset count while preserving category breakdown at DEBUG level.

Also normalize generic server start/stop messages so Game Server and RCON Server are labeled correctly instead of being glued to host:port output.
2026-06-13 18:24:02 +02:00
simoleo89 ea55258979 style(startup): use universal console splash
Replace the temporary ASCII-art banner with a structured startup splash that uses plain ASCII, aligned fields, and no ANSI or terminal-specific features. This keeps the emulator startup readable across CMD, PowerShell, Linux terminals, Docker logs, CI output, and copied log files. Add a contract test to keep the splash universal.
2026-06-13 18:24:02 +02:00
simoleo89 16d89cdb31 style(startup): customize emulator console banner
Add a clean ASCII startup banner for the emulator CMD window and use it instead of the legacy wide block logo. The new banner stays ASCII-only for Windows console compatibility and keeps the Morningstar identity visible before the startup logs.
2026-06-13 18:24:01 +02:00
simoleo89 ede7eb8284 style(startup): tidy console banner logs
Shorten the infostand background startup message into a compact asset summary and print the project/version/build details as a single ASCII startup card instead of several timestamped log lines. Add a small contract test for the compact infostand summary format.
2026-06-13 18:24:01 +02:00
simoleo89 216078f62c fix(messages): silence duplicate packet aliases
PacketNames reflects public static final packet constants and warns when two names share the same header. RequestCatalogIndexEvent is a legacy alias for the active Builders Club catalog index header, and InClientLinkComposer shares the NUX link payload/header. Keep those aliases available to existing code while removing them from the reflected packet-name set, and add a contract test so future public final packet names stay unique.
2026-06-13 18:23:57 +02:00
simoleo89 0f15371676 fix(marketplace): only pay out claimed offers after detach
MarketPlace.getCredits previously removed sold offers from memory and granted credits before knowing whether marketplace_items.user_id had been detached in the database. If that update failed, the same sold offer could be loaded as claimable again later. Make removeUser report success, keep the offer claimable on failure, and only grant credits after the database detach succeeds.
2026-06-13 18:23:37 +02:00
simoleo89 c25cb2a9b6 fix(trading): abort item exchange when persistence fails
RoomTrade previously caught SQLException during ownership updates but continued into the in-memory inventory and credit transfer path. That could desynchronize or duplicate trade results if the database batch failed while the live session still completed the exchange. Keep item owner mutations after the successful batch, return offered items on failed completion, and add a contract test that prevents SQL failures from falling through to the transfer path.
2026-06-13 18:23:33 +02:00
DuckieTM 87e1ef94f7 Merge pull request #169 from duckietm/main
Main to Dev
2026-06-12 15:56:21 +02:00
github-actions[bot] 510e0d082e 🆙 Bump version to 4.2.44 [skip ci] 2026-06-12 13:53:22 +00:00
DuckieTM e13c7fdbb6 Merge pull request #168 from hotellidev/multicolorfurnifix
Fix multicolor furni in furni editor
2026-06-12 15:52:23 +02:00
hotellidev 2a28fbd2e5 Fix multicolor furni in furni editor 2026-06-11 04:07:22 +03:00
github-actions[bot] cd60cba355 🆙 Bump version to 4.2.43 [skip ci] 2026-06-10 13:32:38 +00:00
DuckieTM e62f461962 Merge pull request #167 from duckietm/dev
㊙️ Security updates
2026-06-10 15:31:38 +02:00
duckietm 7f8c98e4f3 ㊙️ Security updates 2026-06-10 15:31:18 +02:00
github-actions[bot] d95e09e64f 🆙 Bump version to 4.2.42 [skip ci] 2026-06-10 13:10:40 +00:00
DuckieTM ebe0690e46 Merge pull request #166 from duckietm/dev
🆙 Fix multiheight
2026-06-10 15:09:31 +02:00
duckietm 0dda0ae0f7 🆙 Fix multiheight 2026-06-10 15:09:14 +02:00
github-actions[bot] 54ab6613f1 🆙 Bump version to 4.2.41 [skip ci] 2026-06-10 12:18:32 +00:00
DuckieTM 9fda766ba5 Merge pull request #165 from duckietm/dev
🆙 Fix Group Forum buy
2026-06-10 14:17:32 +02:00
duckietm 3da9325344 🆙 Fix Group Forum buy 2026-06-10 14:17:17 +02:00
github-actions[bot] 770739c256 🆙 Bump version to 4.2.40 [skip ci] 2026-06-10 08:16:33 +00:00
DuckieTM 3ec468993a Merge pull request #164 from duckietm/dev
Dev
2026-06-10 10:15:20 +02:00
duckietm 0e0f1cbb15 🆙 Navigator Group Filter 2026-06-10 10:14:49 +02:00
DuckieTM daeda761cd Merge pull request #163 from simoleo89/feat/security-concurrency-economy-hardening
Security, concurrency & economy hardening + dependency upgrades and modernization
2026-06-10 06:43:45 +02:00
DuckieTM 0906048a3a Merge pull request #162 from RemcoEpicnabbo/main
Handle '.' in vending_ids parsing
2026-06-10 06:43:31 +02:00
simoleo89 19cde45d3e fix(marketplace): avoid inventory desync on failed offer insert
Expose whether a marketplace offer was persisted before mutating inventory state, refuse sells whose database insert failed, and synchronize the sold timestamp into the online seller's in-memory offer when present. This keeps failed or racing marketplace operations from desynchronizing credits/items.
2026-06-09 22:02:53 +02:00
simoleo89 8161e3d7e5 fix(moderation): harden ban and modtool edge cases
Use executeUpdate with generated keys for offline ban inserts, return an empty result when an offline target cannot be loaded, and make ban commands handle empty results instead of indexing blindly. Modtool chatlog requests now guard missing users instead of dereferencing null.
2026-06-09 22:02:33 +02:00
simoleo89 5c0f2d2855 fix(session): separate forced disconnects from resume parking
Add a forced dispose path for bans, RCON disconnects, logout/account endpoints, plugin-cancelled login, duplicate login replacement, and late MAC-ban enforcement. Soft channel closes still park a session for reconnect, while security-driven closes now bypass session resume. Also null-guard client/channel disposal and cover the contract with focused tests.
2026-06-09 22:02:07 +02:00
simoleo89 d984461cc0 fix(login): don't reject login when the machine fingerprint arrives after the SSO ticket
The Nitro renderer sends the UniqueID (machine fingerprint) packet right AFTER the SSOTicket, so Habbo.connect() ran before the machineId was set and returned false on the empty machineId — silently disposing the client (WS closed with Netty's default "Bye"), so login never completed and no SecureLoginOK was sent.

- Habbo.connect(): only set machineID + run the MAC-ban check when the fingerprint is already present; never reject the login solely for a missing machineId (also drop a duplicated MAC/IP-ban block).

- MachineIDEvent: enforce the MAC ban when the fingerprint arrives after login, preserving the ban check that connect() now defers.
2026-06-09 20:50:12 +02:00
simoleo89 61ea33ac28 docs(config): document new networking/threading keys from the hardening batch
Add commented examples for the config keys introduced by this PR so operators
can discover and tune them (defaults apply if unset):
- ws.ip.header.trusted (trusted reverse-proxy gate for the forwarded-IP header)
- io.packet.handler.threads (game packet-handler pool, off the Netty I/O loop)
- auth.http.pool.size (dedicated /api/auth/* worker pool)
- io.netty.allocator.pooled (opt-in pooled ByteBuf allocator)
2026-06-09 20:06:31 +02:00
simoleo89 b6ee400b83 refactor: drop Joda-Time (-> java.time) and make protocol charsets explicit
Modernization following the dependency upgrades:
- Joda-Time was used in exactly one place (ModToolSanctionInfoComposer, to
  subtract probation days from a Date). Migrated to java.time
  (Instant/ZoneId.systemDefault, calendar-accurate like the old Joda call) and
  removed the joda-time dependency entirely — confirmed gone from the shaded jar.
- Make string<->bytes conversions explicitly UTF-8 instead of relying on the
  platform default. Most importantly the wire codec (ClientMessage.readString /
  ServerMessage.appendString) — both sides now pinned to UTF-8 so international
  characters are robust regardless of -Dfile.encoding. Also RCONServerHandler,
  PluginManager and the WS origin-forbidden response.

Verified: clean compile, 15/15 tests, shaded jar.
2026-06-09 20:05:31 +02:00
simoleo89 62104596ac refactor(netty): migrate off the deprecated NioEventLoopGroup (Netty 4.2)
Netty 4.2 deprecates NioEventLoopGroup in favour of the generic
MultiThreadIoEventLoopGroup driven by an IoHandlerFactory. Server.java now
builds its boss/worker groups with MultiThreadIoEventLoopGroup(..,
NioIoHandler.newFactory()) — functionally equivalent, and the codebase now
compiles with zero deprecation warnings. Verified: compile, 15/15 tests, shaded
jar.
2026-06-09 20:05:31 +02:00
simoleo89 fad6be158a chore(deps): upgrade Netty (4.2), HikariCP (7) and JUnit (6) to latest major
Major-version upgrades of the three the previous bump deliberately held back.
Verified: clean compile, all 15 tests run green (surefire 3.5.2 drives the JUnit 6
platform fine — no extra launcher dep needed), and the shaded jar assembles.

- io.netty:netty-all            4.1.135.Final -> 4.2.15.Final
- com.zaxxer:HikariCP           6.3.3         -> 7.0.2
- org.junit.jupiter:junit-jupiter 5.14.4      -> 6.1.0

Notes:
- Stayed on Netty 4.2 (GA), not 5.0 which is still Alpha. No source changes
  needed; the channel ALLOCATOR is set explicitly so 4.2's new default adaptive
  allocator doesn't apply. NioEventLoopGroup is deprecated in 4.2 but still
  functions as before (left as-is to avoid an event-loop behavioural change).
  netty-all 4.2 pulls more transitive modules, so the fat jar grows (~20->32 MB).
- HikariCP 7.x baselines Java 17; our HikariConfig usage is unchanged.
2026-06-09 20:05:31 +02:00
simoleo89 a9f1903465 chore(deps): update dependencies to latest stable
Bumped to the latest stable within each safe major line (no source changes
needed — all APIs compatible; verified with clean compile + test + shaded jar):

- io.netty:netty-all            4.1.118 -> 4.1.135.Final
- com.google.code.gson:gson     2.11.0  -> 2.14.0
- org.mariadb.jdbc:*            3.5.1    -> 3.5.8
- com.zaxxer:HikariCP           6.2.1    -> 6.3.3
- org.apache.commons:commons-lang3 3.17.0 -> 3.20.0
- org.jsoup:jsoup               1.18.3   -> 1.22.2
- org.slf4j:slf4j-api           2.0.16   -> 2.0.18
- ch.qos.logback:logback-classic 1.5.15  -> 1.5.34
- org.fusesource.jansi:jansi    2.4.1    -> 2.4.3
- joda-time:joda-time           2.13.0   -> 2.14.2
- org.eclipse.angus:jakarta.mail 2.0.3   -> 2.0.5
- org.junit.jupiter:junit-jupiter 5.10.2 -> 5.14.4
- maven-surefire-plugin         3.2.5    -> 3.5.2

Deliberately NOT changed:
- Stayed on the netty 4.1.x line (4.2/5.0 are new majors with API changes),
  slf4j 2.0.x (logback 1.5.x requires it), junit 5.x (6.x needs a new baseline),
  and HikariCP 6.x (kept the current major for DB-pool stability; 7.x available).
- trove4j 3.0.3, commons-math3 3.6.1, jbcrypt 0.4 — already at their final
  (unmaintained) releases.
- compiler source/target=19 / release=21 — intentional per project convention.
2026-06-09 20:05:31 +02:00
simoleo89 af82352f24 feat: configurable pool sizes (#2) + pool-safe buffers and opt-in pooled allocator (#5)
#2 — tunable thread pools (sensible defaults kept):
- io.packet.handler.threads overrides the packet-handler EventExecutorGroup size
  (default max(16, 2x cores)).
- auth.http.pool.size overrides the auth HTTP pool max threads (default 16).

#5 — Netty buffer pooling:
- Make the crypto handlers pool-safe: GameByteEncryption/GameByteDecryption no
  longer call ByteBuf.array() on a readBytes-derived buffer (whose arrayOffset is
  non-zero under a pooled allocator, which would have read/encrypted the wrong
  region). They now copy the readable region into a plain byte[] (offset-safe)
  and wrap the result — also drops one intermediate buffer allocation. This is
  correct for the current unpooled allocator too. (ServerMessage uses its own
  Unpooled buffer, and ClientMessage reads via buffer methods, so both are
  already offset-safe.)
- Add a shared channel allocator selected by io.netty.allocator.pooled
  (default false = unpooled-heap, unchanged). Set true for a pooled HEAP
  allocator (preferDirect=false, so array-backed paths keep working) to cut
  per-packet alloc/GC churn. Opt-in until validated under load with the Netty
  leak detector, since unreleased pooled buffers accumulate rather than being
  GC-reclaimed.

New optional config keys (insert into emulator_settings to set/silence the
"key not found" notice): io.packet.handler.threads, auth.http.pool.size,
io.netty.allocator.pooled.
2026-06-09 20:05:30 +02:00
simoleo89 dcc23ba744 feat: housekeeping audit log + shared Gson instances
Security:
- HousekeepingAuditLog: append-only audit trail of privileged actions. There was
  no record of which operator granted ranks/currency to whom. SetUserRank,
  GiveCredits and GiveCurrency now log operator id+name, action, target, detail
  and IP. Writes are async; the housekeeping_log table is created on first use
  (CREATE TABLE IF NOT EXISTS) so no manual migration is needed.

Speed (minor):
- RCONServerHandler / PluginManager: reuse a shared Gson instead of allocating a
  new parser per request/plugin-config load (Gson is thread-safe). The wired
  Gson builders were already cached singletons.
2026-06-09 20:05:30 +02:00
simoleo89 f7556138aa feat: LIKE-wildcard escaping (security) + recycle/craft reward rollback (stability)
Security / speed:
- New util SqlLikeEscaper: escapes %, _ and \ in user search input. Applied to
  the user-facing LIKE searches (messenger user search, marketplace search,
  furni-editor search, housekeeping room search, guild member search) so a query
  like "%" can no longer match everything or trigger a needless full scan, and
  usernames containing "_" are matched literally.

Stability (item-loss fixes):
- RecycleEvent: compute the recycler reward BEFORE consuming the 8 inputs. The
  inputs were deleted from the DB first, so a null reward (misconfig) destroyed
  them permanently with nothing back. Now the inputs are only removed once the
  reward is confirmed.
- CraftingCraftItemEvent: restore the pulled ingredients to the inventory if the
  recipe can't be completed (not enough ingredients mid-pull, or reward creation
  returns null) — previously they silently vanished from the inventory.
2026-06-09 20:05:30 +02:00
simoleo89 a0910d822c fix: deep-analysis pass — self-review regressions + pre-existing logic bugs
Regressions found by an adversarial review of this branch's own diff:
- RoomCycleManager: stop holding the currentBots/currentPets monitor across the
  whole bot/pet tick — snapshot under the lock then cycle off-lock. The previous
  fix blocked place/pickup and room dispose for the full tick and inverted lock
  order vs roomUnitLock->currentBots (latent deadlock for any future cycle code
  touching roomUnitLock).
- HabboInfo: complete the currencyLock invariant — getCurrencies() now returns a
  snapshot under the lock (UserInfoCommand iterated the live Trove map off-lock,
  the exact rehash corruption the lock guards); canBuy() uses the lock-guarded
  getCredits()/getCurrencyAmount(); run() reads credits under the lock for save.
- RoomSpecialTypes: synchronize the by-id pet getters (getNest/getPetDrink/
  getPetFood/getPetToy/getPetTree) to match their now-synchronized mutators.
- AuthHttpUtil.isTrustedProxy: exact-match trusted IPs; only treat an entry as a
  range when it ends with "."/":" so "10.0.0.1" can't also trust "10.0.0.12".

Pre-existing logic bugs found by the deep subsystem analysis:
- RoomUsersComposer: the bulk (room-entry) branch wrote the guild id twice;
  the second field must be the 1/-1 group-membership flag (matches the single
  branch) — every user showed a wrong group indicator on room entry.
- BotManager.pickUpBot: room owners (and ACC_PLACEFURNI) couldn't pick up bots
  placed in their own room — added the room-owner clause that placeBot has.
- PetPickupEvent: compared user id to pet.getId() instead of pet.getUserId(), so
  a pet owner who isn't the room owner couldn't pick up their own pet.
- RoomRightsManager.refreshRightsForHabbo: in guild rooms, explicit room_rights
  were stripped (overwritten by guild level NONE); now takes the stronger of
  explicit rights and guild level.
- RoomRequestBannedUsersEvent: `!hasRights || !ACC_ANYROOMOWNER` required BOTH,
  denying legitimate owners the banned-users list — corrected to `&&`.
- InteractionPetBreedingNest.breed: a crafted packet on a not-full nest deleted
  the nest furni then NPE'd (furni loss); guard petOne/petTwo/room before the
  destructive delete; ConfirmPetBreedingEvent null-checks the room.
- WiredEffectTeleport/UserFurniBase: appended item id instead of sprite id in the
  incompatible-triggers list (cosmetic wired-dialog mismatch) — matched the ~10
  other effects' getBaseItem().getSpriteId() convention.
2026-06-09 20:05:30 +02:00
simoleo89 4eb1484daf perf: run game packet handlers off the Netty I/O loop + bound A* pathfinding (P2)
Root cause from the CPU audit: every incoming packet handler ran on the Netty
I/O event loop (MULTI_THREADED_PACKET_HANDLING is false by default), so any
blocking handler — login DB + loadHabbo, friends/polls/catalog/guild-forum
JDBC (~48 handlers), synchronous A* per walk — stalled socket I/O for every
other client sharing that I/O thread.

- WebSocketChannelInitializer: register GameMessageHandler on a dedicated
  DefaultEventExecutorGroup (max(16, 2x cores), daemon). Netty pins each channel
  to one executor in the group, so a client's packets stay strictly ordered (no
  new intra-client races) while blocking work moves off the I/O loop. The
  cross-client concurrency degree matches the already-multi-threaded I/O group,
  and this is strictly safer than the existing (order-losing) shared-pool
  MULTI_THREADED_PACKET_HANDLING mode the codebase already supported.
- GameMessageHandler: always run the handler inline (now on the group thread);
  drop the shared-pool branch (which would break per-channel ordering and also
  removes the rejectable-pool ByteBuf-drop path).
- PathfinderImpl: default the A* execution-time guard ON (25ms) so a pathological
  search returns an empty path instead of running unbounded on its thread.

Note: this changes the server's packet-threading model — verified to compile,
unit-test, and assemble the shaded jar, but should be load-tested before prod.
Group size is currently derived from CPU count; can be made a config key if
tuning is needed.
2026-06-09 20:05:30 +02:00
simoleo89 45d01876c1 fix: bound the move-blocking Future.get in RoomUserWalkEvent
roomUnit.getMoveBlockingTask().get() blocked the Netty event loop with no
timeout; a stuck/delayed move-blocking task would park the worker thread (and
every client on it) indefinitely. Wait at most 2s, then proceed with the walk.
2026-06-09 20:05:30 +02:00
simoleo89 1c4449fb88 perf: run auth HTTP endpoints off the Netty event loop (P1)
The /api/auth/* handlers ran inline on the Netty worker event loop, so their
blocking work — BCrypt (cost 12 ~tens of ms), JDBC, the Turnstile HTTPS
round-trip and SMTP — stalled every other client multiplexed on that thread; a
burst of logins/registers could freeze game traffic.

Dispatch each auth request to a dedicated bounded pool (4..16 daemon threads,
bounded queue, 503 on saturation) instead. It is deliberately SEPARATE from the
shared game ThreadPooling so auth load can't starve room cycles either. Netty
writes are thread-safe, so the endpoints' sendJson calls work unchanged from the
worker; the FullHttpRequest is released when the task finishes.

Caveat: this allows concurrent handling of pipelined requests on a single
keep-alive connection (out-of-order responses) — not a concern for the Nitro
client which is strictly request/response, but worth load-testing before prod.
2026-06-09 20:05:29 +02:00
simoleo89 373d0399c1 fix: trusted-proxy gate for forwarded IP, wired-var cache + ghost-session cleanup
Security (S3):
- AuthHttpUtil/WebSocketHttpHandler: only honour the configured ws.ip.header
  forwarded-IP header when the DIRECT peer is a trusted reverse proxy, instead
  of trusting it unconditionally. Loopback is always trusted; extra proxies can
  be allow-listed (exact IP or string prefix, comma-separated) via the new
  `ws.ip.header.trusted` config key — default-deny so the header can't be
  spoofed from the open internet to evade per-IP rate limiting and IP bans.
  Also take only the first comma token when setting the game-session WS_IP.

Leak cleanup (C4):
- WiredVariableReferenceSupport.invalidateRoom(): drop a room's shared
  wired-variable assignment caches; called from Room.dispose so the static
  USER/ROOM_ASSIGNMENT_CACHE maps don't retain entries for the JVM lifetime.
- SessionResumeManager.parkHabbo: if the scheduler refuses the grace-expiry
  task (future == null), disconnect immediately instead of parking an
  un-reapable GhostSession that would pin the Habbo + room refs forever.

Note: ws.ip.header.trusted defaults to loopback-only; deployments whose proxy
is on another host must add its IP/prefix to that key or client IPs will
collapse to the proxy address.
2026-06-09 20:05:29 +02:00
simoleo89 01c17c0511 fix: wired double-fire guard, RoomUnit path race, roomItems iteration, Netty CVE
Continuation of the concurrency hardening from the audit:
- InteractionWired/WiredHandler (E4): add an atomic per-box processing guard so
  one trigger box is handled by a single thread at a time, making the cooldown
  check-and-set effectively atomic; mark `cooldown` volatile. Prevents a packet
  thread and the room cycle thread from double-firing the same wired stack
  (double teleport/reward).
- RoomUnit (C1): the walk path is now a volatile ConcurrentLinkedDeque instead of
  a plain LinkedList, so the room cycle popping steps can't corrupt it while a
  walk packet rebuilds it via findPath/setPath.
- RoomItemManager (C2): iterate roomItems under its own monitor in getFloorItems/
  getWallItems/getPostItNotes/getUserUniqueFurniCount/getItemsAt, matching the
  existing put/remove sync sites — stops place/pickup from corrupting the
  traversal into a silently-incomplete item set.
- pom.xml (S4): bump netty-all 4.1.115 -> 4.1.118.Final (CVE-2025-24970 SslHandler
  pre-auth DoS, CVE-2025-25193).
2026-06-09 20:05:29 +02:00
simoleo89 d1570d3574 fix: economy-integrity, currency thread-safety, and resource-leak hardening
From the full-codebase audit. Economy/security (Batch A):
- CatalogBuyItemEvent: clamp client `count` to 1..100 — the club-offer branch
  accumulated cost in plain ints, so a huge count overflowed to a negative
  total, bypassed the affordability checks and CREDITED the buyer (free
  currency/subscription exploit).
- HousekeepingGiveCredits/GiveCurrency: bound `amount` to +/-1e9 to stop
  overflow/negative-balance grants via the privileged path.
- RoomTrade: synchronize accept/confirm/offer/remove and add a `completed`
  re-entry guard so two simultaneous confirms can't run tradeItems() twice
  (item/credit duplication).
- HabboInfo: serialize credits + currencies read-modify-write and the
  saveCurrencies snapshot on a dedicated lock (never held across DB I/O) —
  fixes lost updates and Trove rehash-during-iteration corruption between the
  credit-roller thread and purchase/trade handlers.
- AchievementManager/HabboStats: atomic incrementProgress() so concurrent
  progress sources don't lose updates.

Resource/stability (Batch B):
- GameMessageRateLimit: release the wrapped ByteBuf on every drop path
  (ClientMessage isn't ReferenceCounted, so the decoder's auto-release is a
  no-op) — fixes a refcount leak on pre-auth/rate-limited packets.
- AuthRateLimiter: opportunistically evict window-expired STATE/PROBE_STATE
  entries — previously grew unbounded, one entry per unique client IP.
- ForumThread/ForumThreadComment: close getGeneratedKeys() ResultSets via
  try-with-resources, and create the first comment after the thread's
  connection is released (was holding two pooled connections at once).
- DatabasePool: add socketTimeout/connectTimeout/tcpKeepAlive so a stalled
  MariaDB can't pin a pooled connection (and its thread) indefinitely.

Concurrency visibility (Batch C, partial):
- Room: mark allowBotsWalk/allowPets/allowPetsEat volatile (read every cycle,
  written from settings handlers on another thread).
2026-06-09 20:05:29 +02:00
simoleo89 c98d3a3205 fix: guard double gift-open and harden client string reads
- InteractionGift/OpenRecycleBoxEvent: add an atomic open-once guard so two
  near-simultaneous OpenRecycleBox packets can't both schedule the async,
  delayed OpenGift before the wrapper is removed (redundant double-process).
- ClientMessage.readString: treat the length prefix as unsigned (mask 0xFFFF)
  and clamp to the buffered bytes, so a bogus/oversized length no longer
  throws mid-read and desyncs the remaining fields of the packet.
2026-06-09 20:05:29 +02:00
simoleo89 da1fd01074 fix: address bug-hunt findings across security, concurrency, trade & wired
Security
- HousekeepingSetUserRankEvent: add rank-ceiling guard (reject granting a
  rank above the operator's own and modifying a higher-ranked target),
  mirroring GiveRankCommand — closes a privilege-escalation path.

Trade integrity
- RoomTrade.clearAccepted now also resets confirmed; a stale confirmed=true
  let a user strip their side and still complete once the partner re-confirms.

Concurrency
- RoomCycleManager: iterate the synchronized bot/pet maps under their own
  monitor (lock order stays one-directional vs addBot/addPet — no deadlock).
- RoomSpecialTypes: synchronize nest/petDrink/petFood/petToy/petTree writers
  on the same monitor their getters already use.
- HabboStats: synchronize achievement-progress map accessors.
- RebugKickBallAction: drop redundant direct mutation of the shared tile-cache
  sets (updateTile invalidates them right after) — removes a data race.

Robustness
- Wired legacy parsers (HabboCount, NotHabboCount, MatchStatePosition,
  MoveRotateFurni): guard length/format so one malformed row no longer aborts
  the whole room's wired load.
- RoomLayout: fill malformed/short heightmap rows with INVALID tiles instead
  of leaving nulls, and bounds-check door coordinates.
- FurnidataWatcher: defer (instead of drop) a throttled delta so furni-name
  changes are never lost between broadcasts.
- GuildManager.getGuildMembers: fix LIMIT row-count (page size 14, not
  offset+14) so member pages no longer overlap from page 1 on.
2026-06-09 20:05:29 +02:00
Remco Epicnabbo f7bd452cb0 Handle '.' in vending_ids parsing
Normalize vending_ids by replacing semicolons and dots with commas before splitting. This ensures values separated by '.' are treated like other delimiters and parsed correctly as integers, avoiding parsing errors from unexpected separators.
2026-06-09 17:20:02 +02:00
duckietm 48fcd3f78b 🆙 Update SQL 2026-06-08 11:26:03 +02:00
DuckieTM 1275254fa0 Merge pull request #161 from duckietm/main
Sync Main to DEV
2026-06-08 07:31:07 +02:00
duckietm 7ed7a1ec5a 🆙 update the SQL 2026-06-08 07:18:37 +02:00
DuckieTM 1f4eef8e2e 🆙 Added null check to wall /floor and background 2026-06-07 23:14:25 +02:00
596 changed files with 15195 additions and 1810 deletions
+1
View File
@@ -3,6 +3,7 @@ name: Build & Release JAR
on: on:
push: push:
branches: [main] branches: [main]
workflow_dispatch:
permissions: permissions:
contents: write contents: write
@@ -435,6 +435,16 @@ ON DUPLICATE KEY UPDATE
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`); `triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'),
('commands.description.cmd_add_youtube_playlist', ':add_youtube <base_item_id> <youtube_playlist_id>'),
('commands.description.cmd_disablemassmentions', ':disablemassmentions'),
('commands.description.cmd_disablementions', ':disablementions'),
('commands.description.cmd_give_prefix', ':giveprefix <username> <text> <color> [icon] [effect]'),
('commands.description.cmd_hidewired', ':hidewired'),
('commands.description.cmd_list_prefixes', ':listprefixes <username>'),
('commands.description.cmd_remove_prefix', ':removeprefix <username> <id|all>'),
('commands.description.cmd_setroom_template', ':setroom_template'),
('commands.description.cmd_update_youtube_playlists', ':update_youtube'),
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'), ('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'), ('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'), ('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
@@ -37,6 +37,10 @@ VALUES
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
`comment` = VALUES(`comment`); `comment` = VALUES(`comment`);
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_disablementions', ':disablementions'),
('commands.description.cmd_disablemassmentions', ':disablemassmentions');
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- 3. Emulator settings: cooldowns, caps and alias lists -- 3. Emulator settings: cooldowns, caps and alias lists
+47 -1
View File
@@ -19,4 +19,50 @@ CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
('items.furnidata.edit.backup.keep','10'), ('items.furnidata.edit.backup.keep','10'),
('items.furnidata.edit.ratelimit.ms','2000'); ('items.furnidata.edit.ratelimit.ms','2000'),
-- Server-authoritative furni names (source of truth = furnidata JSON)
('items.furnidata.names.enabled','true'),
('items.furnidata.path',''),
('items.furnidata.max.bytes','67108864'),
-- Live-reload watcher
('items.furnidata.watch.enabled','true'),
('items.furnidata.watch.debounce.ms','750'),
('items.furnidata.watch.min.interval.ms','5000'),
('items.furnidata.delta.cap','500'),
-- Furni editor: import official names/descriptions from Habbo
('furni.editor.import.url','https://www.habbo.com/gamedata/furnidata_json/1'),
('furni.editor.import.cache.ms','600000');
START TRANSACTION;
DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
CREATE TEMPORARY TABLE cleanup_furnidata_settings (
`key` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL PRIMARY KEY
);
INSERT INTO cleanup_furnidata_settings (`key`) VALUES
('items.furnidata.names.enabled'),
('items.furnidata.path'),
('items.furnidata.max.bytes'),
('items.furnidata.watch.enabled'),
('items.furnidata.watch.debounce.ms'),
('items.furnidata.watch.min.interval.ms'),
('items.furnidata.delta.cap'),
('furni.editor.import.url'),
('furni.editor.import.cache.ms');
-- Preview rows that will be removed.
SELECT es.`key`, es.`value`
FROM emulator_settings es
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
ORDER BY es.`key`;
DELETE es
FROM emulator_settings es
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
-- Preview remaining matching rows inside the transaction.
SELECT COUNT(*) AS remaining_furnidata_settings
FROM emulator_settings es
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
-- Safe default. Change to COMMIT after reviewing the preview.
ROLLBACK;
@@ -0,0 +1,9 @@
-- Navigator search filters - companion to the gameserver fix for the catalog
-- 'Find groups to join!' button (navigator/search/hotel_view/group:).
INSERT IGNORE INTO `navigator_filter` (`key`, `field`, `compare`, `database_query`) VALUES
('anything', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
('roomname', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
('owner', 'getOwnerName', 'equals_ignore_case', 'SELECT rooms.* FROM rooms WHERE rooms.owner_name LIKE ?'),
('tag', 'getTags', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.tags LIKE ?'),
('group', 'getGuildName', 'contains', 'SELECT rooms.* FROM rooms INNER JOIN guilds ON guilds.room_id = rooms.id WHERE guilds.name LIKE ?');
+132
View File
@@ -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.');
@@ -49,6 +49,7 @@ INSERT INTO `permission_definitions` (`permission_key`, `rank_7`, `comment`) VAL
ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`); ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`);
INSERT INTO `emulator_texts` (`key`, `value`) VALUES INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_setroom_template', ':setroom_template'),
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'), ('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'), ('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'), ('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
@@ -301,6 +301,7 @@ INSERT IGNORE INTO `custom_prefixes_catalog`
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command -- GivePrefix command
('commands.description.cmd_give_prefix', ':giveprefix <username> <text> <color> [icon] [effect]'),
('commands.keys.cmd_give_prefix', 'giveprefix'), ('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'), ('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'),
('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'), ('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'),
@@ -308,12 +309,14 @@ INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'), ('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'), ('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'),
-- ListPrefixes command -- ListPrefixes command
('commands.description.cmd_list_prefixes', ':listprefixes <username>'),
('commands.keys.cmd_list_prefixes', 'listprefixes'), ('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'), ('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'),
('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'), ('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'), ('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'),
('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'), ('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'),
-- RemovePrefix command -- RemovePrefix command
('commands.description.cmd_remove_prefix', ':removeprefix <username> <id|all>'),
('commands.keys.cmd_remove_prefix', 'removeprefix'), ('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'), ('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'),
('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'), ('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'),
@@ -1,27 +0,0 @@
-- 021_furnidata_config.sql
-- Seeds the furnidata feature config keys read at runtime by
-- FurnitureTextProvider / FurnidataReader / FurnidataWatcher and
-- FurniEditorImportTextEvent. Without these rows a fresh install logs
-- "Config key not found" for each (ConfigurationManager logs ERROR even
-- when a default is supplied) and the values are not editable from the DB.
--
-- Notes:
-- * *.enabled keys are read via Boolean.parseBoolean → use true/false (NOT 1/0).
-- * items.furnidata.path is intentionally empty: when blank the source is
-- derived from furni.editor.asset.base.path (seeded by 004_furni_editor.sql)
-- → <base>/furnidata (split-tier) or <base>/FurnitureData.json (single file).
-- * Editor write-path keys (items.furnidata.edit.*) are seeded by 020.
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
-- Server-authoritative furni names (source of truth = furnidata JSON)
('items.furnidata.names.enabled','true'),
('items.furnidata.path',''),
('items.furnidata.max.bytes','67108864'),
-- Live-reload watcher
('items.furnidata.watch.enabled','true'),
('items.furnidata.watch.debounce.ms','750'),
('items.furnidata.watch.min.interval.ms','5000'),
('items.furnidata.delta.cap','500'),
-- Furni editor: import official names/descriptions from Habbo
('furni.editor.import.url','https://www.habbo.it/gamedata/furnidata_json/1'),
('furni.editor.import.cache.ms','600000');
@@ -0,0 +1,15 @@
-- Fix NULL room paint columns
--
-- Some legacy/imported rooms have NULL in paper_wall / paper_floor / paper_landscape.
-- The server compares these with .equals("0.0") on room entry, which throws a
-- NullPointerException (RoomManager.openRoom) and prevents the room from loading.
-- This normalizes existing NULL values and re-enforces the NOT NULL DEFAULT '0.0'
-- constraint so it cannot happen again.
UPDATE `rooms` SET `paper_wall` = '0.0' WHERE `paper_wall` IS NULL;
UPDATE `rooms` SET `paper_floor` = '0.0' WHERE `paper_floor` IS NULL;
UPDATE `rooms` SET `paper_landscape` = '0.0' WHERE `paper_landscape` IS NULL;
ALTER TABLE `rooms` MODIFY COLUMN `paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0';
ALTER TABLE `rooms` MODIFY COLUMN `paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0';
ALTER TABLE `rooms` MODIFY COLUMN `paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0';
+10
View File
@@ -15355,7 +15355,9 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.cmd_promote_offer.list', 'All available offers (%amount%):<br>%list%'), ('commands.cmd_promote_offer.list', 'All available offers (%amount%):<br>%list%'),
('commands.cmd_promote_offer.list.entry', '%id%: %title% %description%'), ('commands.cmd_promote_offer.list.entry', '%id%: %title% %description%'),
('commands.description.acc_debug', ':test [header] i:1 s:a b:1'), ('commands.description.acc_debug', ':test [header] i:1 s:a b:1'),
('commands.description.acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'),
('commands.description.cmd_about', ':about'), ('commands.description.cmd_about', ':about'),
('commands.description.cmd_add_youtube_playlist', ':add_youtube <base_item_id> <youtube_playlist_id>'),
('commands.description.cmd_alert', ':alert <username> <message>'), ('commands.description.cmd_alert', ':alert <username> <message>'),
('commands.description.cmd_allow_trading', 'Enables / Disables the tradelock for a user.'), ('commands.description.cmd_allow_trading', 'Enables / Disables the tradelock for a user.'),
('commands.description.cmd_badge', ':badge <username> <badge>'), ('commands.description.cmd_badge', ':badge <username> <badge>'),
@@ -15379,6 +15381,8 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_danceall', ':danceall <dance id>'), ('commands.description.cmd_danceall', ':danceall <dance id>'),
('commands.description.cmd_diagonal', ':diagonal'), ('commands.description.cmd_diagonal', ':diagonal'),
('commands.description.cmd_disable_effects', ':disableffects'), ('commands.description.cmd_disable_effects', ':disableffects'),
('commands.description.cmd_disablemassmentions', ':disablemassmentions'),
('commands.description.cmd_disablementions', ':disablementions'),
('commands.description.cmd_disconnect', ':disconnect <username>'), ('commands.description.cmd_disconnect', ':disconnect <username>'),
('commands.description.cmd_duckets', ':duckets <username> <amount>'), ('commands.description.cmd_duckets', ':duckets <username> <amount>'),
('commands.description.cmd_ejectall', ':ejectall'), ('commands.description.cmd_ejectall', ':ejectall'),
@@ -15395,11 +15399,13 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_freeze_bots', ':freezebots'), ('commands.description.cmd_freeze_bots', ':freezebots'),
('commands.description.cmd_furnidata', ':furnidata'), ('commands.description.cmd_furnidata', ':furnidata'),
('commands.description.cmd_gift', ':gift <username> <itemid>'), ('commands.description.cmd_gift', ':gift <username> <itemid>'),
('commands.description.cmd_give_prefix', ':giveprefix <username> <text> <color> [icon] [effect]'),
('commands.description.cmd_give_rank', ':giverank <username> <rank>'), ('commands.description.cmd_give_rank', ':giverank <username> <rank>'),
('commands.description.cmd_ha', ':ha <message>'), ('commands.description.cmd_ha', ':ha <message>'),
('commands.description.cmd_hal', ':hal <url> <message>'), ('commands.description.cmd_hal', ':hal <url> <message>'),
('commands.description.cmd_hand_item', ':handitem <itemid>'), ('commands.description.cmd_hand_item', ':handitem <itemid>'),
('commands.description.cmd_happyhour', ':happyhour'), ('commands.description.cmd_happyhour', ':happyhour'),
('commands.description.cmd_hidewired', ':hidewired'),
('commands.description.cmd_hoverboard', ':hoverboard'), ('commands.description.cmd_hoverboard', ':hoverboard'),
('commands.description.cmd_hug', ':hug <username>'), ('commands.description.cmd_hug', ':hug <username>'),
('commands.description.cmd_invisible', ':invisible'), ('commands.description.cmd_invisible', ':invisible'),
@@ -15408,6 +15414,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_kill', ':kill <username>'), ('commands.description.cmd_kill', ':kill <username>'),
('commands.description.cmd_kiss', ':kiss <username>'), ('commands.description.cmd_kiss', ':kiss <username>'),
('commands.description.cmd_lay', ':lay'), ('commands.description.cmd_lay', ':lay'),
('commands.description.cmd_list_prefixes', ':listprefixes <username>'),
('commands.description.cmd_machine_ban', ':machineban <username> [reason]'), ('commands.description.cmd_machine_ban', ':machineban <username> [reason]'),
('commands.description.cmd_massbadge', ':massbadge <badge>'), ('commands.description.cmd_massbadge', ':massbadge <badge>'),
('commands.description.cmd_masscredits', ':masscredits <amount>'), ('commands.description.cmd_masscredits', ':masscredits <amount>'),
@@ -15429,6 +15436,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_push', ':push <username>'), ('commands.description.cmd_push', ':push <username>'),
('commands.description.cmd_redeem', ':redeem'), ('commands.description.cmd_redeem', ':redeem'),
('commands.description.cmd_reload_room', ':reload_room'), ('commands.description.cmd_reload_room', ':reload_room'),
('commands.description.cmd_remove_prefix', ':removeprefix <username> <id|all>'),
('commands.description.cmd_roomalert', ':roomalert <message>'), ('commands.description.cmd_roomalert', ':roomalert <message>'),
('commands.description.cmd_roombadge', ':roombadge <badge>'), ('commands.description.cmd_roombadge', ':roombadge <badge>'),
('commands.description.cmd_roomcredits', ':roomcredits <amount>'), ('commands.description.cmd_roomcredits', ':roomcredits <amount>'),
@@ -15444,6 +15452,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_set', ':set info'), ('commands.description.cmd_set', ':set info'),
('commands.description.cmd_setmax', ':setmax <amount>'), ('commands.description.cmd_setmax', ':setmax <amount>'),
('commands.description.cmd_setpublic', ':setpublic'), ('commands.description.cmd_setpublic', ':setpublic'),
('commands.description.cmd_setroom_template', ':setroom_template'),
('commands.description.cmd_setrotation', ':rot;rotation'), ('commands.description.cmd_setrotation', ':rot;rotation'),
('commands.description.cmd_setspeed', ':setspeed <speed>'), ('commands.description.cmd_setspeed', ':setspeed <speed>'),
('commands.description.cmd_setstate', ':ss'), ('commands.description.cmd_setstate', ':ss'),
@@ -15486,6 +15495,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.description.cmd_update_polls', ':update_polls'), ('commands.description.cmd_update_polls', ':update_polls'),
('commands.description.cmd_update_texts', ':update_texts'), ('commands.description.cmd_update_texts', ':update_texts'),
('commands.description.cmd_update_wordfilter', ':update_word_filter'), ('commands.description.cmd_update_wordfilter', ':update_word_filter'),
('commands.description.cmd_update_youtube_playlists', ':update_youtube'),
('commands.description.cmd_userinfo', ':userinfo <username>'), ('commands.description.cmd_userinfo', ':userinfo <username>'),
('commands.description.cmd_welcome', ':welcome <username>'), ('commands.description.cmd_welcome', ':welcome <username>'),
('commands.description.cmd_word_quiz', ':wordquiz <question>'), ('commands.description.cmd_word_quiz', ':wordquiz <question>'),
+44 -20
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId> <groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId> <artifactId>Habbo</artifactId>
<version>4.2.39</version> <version>4.2.44</version>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -66,7 +66,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version> <version>3.5.2</version>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@@ -83,21 +83,21 @@
<dependency> <dependency>
<groupId>io.netty</groupId> <groupId>io.netty</groupId>
<artifactId>netty-all</artifactId> <artifactId>netty-all</artifactId>
<version>4.1.115.Final</version> <version>4.2.15.Final</version>
</dependency> </dependency>
<!-- GSON --> <!-- GSON -->
<dependency> <dependency>
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
<version>2.11.0</version> <version>2.14.0</version>
</dependency> </dependency>
<!-- MariaDB Connector/J (native driver for MariaDB) --> <!-- MariaDB Connector/J (native driver for MariaDB) -->
<dependency> <dependency>
<groupId>org.mariadb.jdbc</groupId> <groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId> <artifactId>mariadb-java-client</artifactId>
<version>3.5.1</version> <version>3.5.8</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
@@ -113,7 +113,38 @@
<dependency> <dependency>
<groupId>com.zaxxer</groupId> <groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId> <artifactId>HikariCP</artifactId>
<version>6.2.1</version> <version>7.0.2</version>
<scope>compile</scope>
</dependency>
<!-- Caffeine cache - high-performance local caching for hot emulator lookups -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.4</version>
<scope>compile</scope>
</dependency>
<!-- Resilience4j - rate limits and circuit breakers for RCON/HTTP/external integrations -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>2.4.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>2.4.0</version>
<scope>compile</scope>
</dependency>
<!-- Hibernate Validator - Jakarta Bean Validation for packet/RCON/admin DTO guards -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.1.0.Final</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
@@ -121,7 +152,7 @@
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.17.0</version> <version>3.20.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
@@ -137,7 +168,7 @@
<dependency> <dependency>
<groupId>org.jsoup</groupId> <groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId> <artifactId>jsoup</artifactId>
<version>1.18.3</version> <version>1.22.2</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
@@ -145,14 +176,14 @@
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>2.0.16</version> <version>2.0.18</version>
</dependency> </dependency>
<!-- Logback --> <!-- Logback -->
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<version>1.5.15</version> <version>1.5.34</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
@@ -160,14 +191,7 @@
<dependency> <dependency>
<groupId>org.fusesource.jansi</groupId> <groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId> <artifactId>jansi</artifactId>
<version>2.4.1</version> <version>2.4.3</version>
</dependency>
<!-- Joda Time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.13.0</version>
</dependency> </dependency>
<!-- jBCrypt used by the built-in /api/auth/* HTTP login handler <!-- jBCrypt used by the built-in /api/auth/* HTTP login handler
@@ -183,14 +207,14 @@
<dependency> <dependency>
<groupId>org.eclipse.angus</groupId> <groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId> <artifactId>jakarta.mail</artifactId>
<version>2.0.3</version> <version>2.0.5</version>
</dependency> </dependency>
<!-- JUnit Jupiter --> <!-- JUnit Jupiter -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
<version>5.10.2</version> <version>6.1.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
+157 -18
View File
@@ -18,6 +18,8 @@ import com.eu.habbo.plugin.events.emulator.EmulatorStartShutdownEvent;
import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent; import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent;
import com.eu.habbo.threading.ThreadPooling; import com.eu.habbo.threading.ThreadPooling;
import com.eu.habbo.util.imager.badges.BadgeImager; import com.eu.habbo.util.imager.badges.BadgeImager;
import com.eu.habbo.util.logback.ConsoleStyle;
import org.fusesource.jansi.AnsiConsole;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -38,6 +40,12 @@ public final class Emulator {
private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class); private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class);
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown"); private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown"); private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
private static final String ANSI_RESET = "\u001B[0m";
private static final String ANSI_BOLD = "\u001B[1m";
private static final String ANSI_CYAN = "\u001B[36m";
private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_DIM = "\u001B[2m";
// Fallback version, only used when running outside a packaged jar (e.g. from // Fallback version, only used when running outside a packaged jar (e.g. from
// the IDE). In production the version comes from the jar manifest below. // the IDE). In production the version comes from the jar manifest below.
@@ -65,7 +73,6 @@ public final class Emulator {
"██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝███████║ ██║ ██║ ██║██║ ██║\n" + "██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝███████║ ██║ ██║ ██║██║ ██║\n" +
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\n" + "╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\n" +
"Still Rocking in 2026.\n"; "Still Rocking in 2026.\n";
public static String build = ""; public static String build = "";
public static long buildTimestamp = -1L; public static long buildTimestamp = -1L;
@@ -104,14 +111,12 @@ public final class Emulator {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
try { try {
if (OS_NAME.startsWith("Windows") && !CLASS_PATH.contains("idea_rt.jar")) { boolean styledConsole = shouldStyleConsole(
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); System.getenv(),
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console"); System.console() != null,
OS_NAME,
appender.stop(); System.getProperty("habbo.console.style", "auto"));
appender.setWithJansi(true); configureAnsiConsole(styledConsole);
appender.start();
}
Locale.setDefault(Locale.of("en")); Locale.setDefault(Locale.of("en"));
setBuild(); setBuild();
@@ -119,7 +124,7 @@ public final class Emulator {
ConsoleCommand.load(); ConsoleCommand.load();
Emulator.logging = new Logging(); Emulator.logging = new Logging();
System.out.println(logo); System.out.println(startupHero(styledConsole));
long startTime = System.nanoTime(); long startTime = System.nanoTime();
@@ -153,14 +158,21 @@ public final class Emulator {
Emulator.config.register("camera.price.points.type", "5"); Emulator.config.register("camera.price.points.type", "5");
Emulator.config.register("camera.render.delay", "5"); Emulator.config.register("camera.render.delay", "5");
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.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", "");
Emulator.config.register("rcon.max_payload_bytes", "65536");
Emulator.config.register("nitro.secure.api.max_payload_bytes", "65536");
Emulator.config.register("nitro.secure.config.max_file_bytes", "2097152");
Emulator.config.register("nitro.secure.gamedata.max_file_bytes", "16777216");
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(); System.out.println(startupCard(hotelTimezoneId));
LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, ");
System.out.println();
LOGGER.info("This project is for educational purposes only. This Emulator is an open-source fork of Arcturus created by TheGeneral.");
LOGGER.info("Version: {}", version);
LOGGER.info("Build: {}", build);
LOGGER.info("Build Timestamp: {} [{}]", formatBuildTimestamp(buildTimestamp, hotelTimezoneId), 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!");
Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture."); Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture.");
Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*"); Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*");
@@ -198,7 +210,7 @@ public final class Emulator {
Emulator.isReady = true; Emulator.isReady = true;
Emulator.timeStarted = getIntUnixTimestamp(); Emulator.timeStarted = getIntUnixTimestamp();
if (Emulator.getConfig().getBoolean("gui.enabled", true)) { if (shouldLaunchGui()) {
EmulatorDashboard.launch(); EmulatorDashboard.launch();
} }
@@ -310,6 +322,97 @@ public final class Emulator {
return -1L; return -1L;
} }
static String startupCard(String hotelTimezoneId) {
return "\n" +
"+----------------------------------------------------------------+\n" +
"| Arcturus Morningstar Extended |\n" +
"| Source : github.com/duckietm/Arcturus-Morningstar-Extended |\n" +
"| Scope : Educational open-source fork by TheGeneral |\n" +
"| Version: " + version + "\n" +
"| Build : " + build + "\n" +
"| Time : " + formatBuildTimestamp(buildTimestamp, hotelTimezoneId) + " [" + hotelTimezoneId + "]\n" +
"+----------------------------------------------------------------+\n";
}
static String startupHero() {
return startupHero(false);
}
static String startupHero(boolean styled) {
if (styled) {
return "\n" +
ANSI_CYAN +
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
ANSI_RESET +
"\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
"| " + ANSI_BOLD + ANSI_GREEN + "[OK] MORNINGSTAR EXTENDED" + ANSI_RESET + fit("", 50) + " |\n" +
"| " + ANSI_DIM + "Arcturus game server runtime" + ANSI_RESET + fit("", 48) + " |\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
"| " + ANSI_YELLOW + "[VER]" + ANSI_RESET + " Version : " + fit(version, 57) + " |\n" +
"| " + ANSI_YELLOW + "[BLD]" + ANSI_RESET + " Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 57) + " |\n" +
"| " + ANSI_YELLOW + "[JVM]" + ANSI_RESET + " Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / styled console output", 57) + " |\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n";
}
return "\n" +
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
"\n" +
"+------------------------------------------------------------------------------+\n" +
"| MORNINGSTAR EXTENDED |\n" +
"| Arcturus game server runtime |\n" +
"+------------------------------------------------------------------------------+\n" +
"| Version : " + fit(version, 63) + " |\n" +
"| Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 63) + " |\n" +
"| Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / universal console output", 63) + " |\n" +
"+------------------------------------------------------------------------------+\n";
}
static boolean shouldStyleConsole(Map<String, String> environment, boolean interactiveConsole, String osName, String styleProperty) {
return ConsoleStyle.isEnabled(environment, interactiveConsole, osName, styleProperty);
}
static void configureAnsiConsole(boolean styledConsole) {
if (!styledConsole || !OS_NAME.startsWith("Windows") || CLASS_PATH.contains("idea_rt.jar")) {
return;
}
try {
AnsiConsole.systemInstall();
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
if (appender != null) {
appender.stop();
appender.setWithJansi(true);
appender.start();
}
} catch (Throwable e) {
LOGGER.debug("Unable to install Jansi console bridge; continuing with raw console output.", e);
}
}
static boolean shouldLaunchGui() {
return Emulator.getConfig() != null && Emulator.getConfig().getBoolean("gui.autostart.enabled", false);
}
private static String fit(String value, int width) {
String safe = value == null ? "" : value;
if (safe.length() > width) {
return safe.substring(0, Math.max(0, width - 3)) + "...";
}
return String.format("%-" + width + "s", safe);
}
private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) { private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) {
if (buildTimestamp <= 0) { if (buildTimestamp <= 0) {
return "UNKNOWN"; return "UNKNOWN";
@@ -390,6 +493,42 @@ 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");
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() { public static RCONServer getRconServer() {
return rconServer; return rconServer;
} }
@@ -52,6 +52,10 @@ public class TextsManager {
return this.texts.getProperty(key, defaultValue); return this.texts.getProperty(key, defaultValue);
} }
public String getValueQuietly(String key, String defaultValue) {
return this.texts.getProperty(key, defaultValue);
}
public boolean getBoolean(String key) { public boolean getBoolean(String key) {
return this.getBoolean(key, false); return this.getBoolean(key, false);
} }
@@ -79,6 +79,14 @@ class DatabasePool {
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true"); databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true"); databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true"); databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
// Fail fast instead of pinning a pooled connection (and its worker
// thread) indefinitely on a stalled/slow MariaDB. HikariCP's
// connectionTimeout only bounds the pool *borrow*; these bound the
// actual socket/connect round-trip. Overridable via db.params.
databaseConfiguration.addDataSourceProperty("socketTimeout", "30000");
databaseConfiguration.addDataSourceProperty("connectTimeout", "10000");
databaseConfiguration.addDataSourceProperty("tcpKeepAlive", "true");
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false"); databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
databaseConfiguration.setPoolName("HabboHikariPool"); databaseConfiguration.setPoolName("HabboHikariPool");
@@ -100,9 +100,9 @@ public class AchievementManager {
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten. if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
return; return;
habbo.getHabboStats().setProgress(achievement, currentProgress + amount); int newProgress = habbo.getHabboStats().incrementProgress(achievement, amount);
AchievementLevel newLevel = achievement.getLevelForProgress(currentProgress + amount); AchievementLevel newLevel = achievement.getLevelForProgress(newProgress);
if (AchievementManager.TALENTTRACK_ENABLED) { if (AchievementManager.TALENTTRACK_ENABLED) {
for (TalentTrackType type : TalentTrackType.values()) { for (TalentTrackType type : TalentTrackType.values()) {
@@ -179,23 +179,31 @@ public class BotManager {
} }
public void pickUpBot(Bot bot, Habbo habbo) { public void pickUpBot(Bot bot, Habbo habbo) {
HabboInfo receiverInfo = habbo == null ? Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId()) : habbo.getHabboInfo();
if (bot != null) { if (bot != null) {
HabboInfo receiverInfo = resolvePickupReceiver(bot, habbo);
Room botRoom = bot.getRoom();
if (receiverInfo == null || botRoom == null) {
return;
}
BotPickUpEvent pickedUpEvent = new BotPickUpEvent(bot, habbo); BotPickUpEvent pickedUpEvent = new BotPickUpEvent(bot, habbo);
Emulator.getPluginManager().fireEvent(pickedUpEvent); Emulator.getPluginManager().fireEvent(pickedUpEvent);
if (pickedUpEvent.isCancelled()) if (pickedUpEvent.isCancelled())
return; return;
if (habbo == null || (bot.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_ANYROOMOWNER))) { Room currentRoom = habbo != null ? habbo.getHabboInfo().getCurrentRoom() : null;
if (habbo == null
|| bot.getOwnerId() == habbo.getHabboInfo().getId()
|| habbo.hasPermission(Permission.ACC_ANYROOMOWNER)
|| (currentRoom != null && (currentRoom.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_PLACEFURNI)))) {
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) { if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + "")); habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
return; return;
} }
bot.onPickUp(habbo, receiverInfo.getCurrentRoom()); bot.onPickUp(habbo, botRoom);
receiverInfo.getCurrentRoom().removeBot(bot); botRoom.removeBot(bot);
bot.stopFollowingHabbo(); bot.stopFollowingHabbo();
bot.setOwnerId(receiverInfo.getId()); bot.setOwnerId(receiverInfo.getId());
bot.setOwnerName(receiverInfo.getUsername()); bot.setOwnerName(receiverInfo.getUsername());
@@ -211,6 +219,14 @@ public class BotManager {
} }
} }
private HabboInfo resolvePickupReceiver(Bot bot, Habbo picker) {
if (picker != null && bot.getOwnerId() == picker.getHabboInfo().getId()) {
return picker.getHabboInfo();
}
return Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId());
}
public Bot loadBot(ResultSet set) { public Bot loadBot(ResultSet set) {
try { try {
String type = set.getString("type"); String type = set.getString("type");
@@ -174,6 +174,14 @@ public class CatalogItem implements ISerialize, Runnable, Comparable<CatalogItem
return this.offerId; return this.offerId;
} }
public int getSearchOfferId() {
if (this.offerId > 0) {
return this.offerId;
}
return haveOffer(this) ? this.id : -1;
}
public boolean isLimited() { public boolean isLimited() {
return this.limitedStack > 0; return this.limitedStack > 0;
} }
@@ -494,10 +494,11 @@ public class CatalogManager {
item = new CatalogItem(set); item = new CatalogItem(set);
page.addItem(item); page.addItem(item);
if (item.getOfferId() != -1) { int searchOfferId = item.getSearchOfferId();
page.addOfferId(item.getOfferId()); if (searchOfferId != -1) {
page.addOfferId(searchOfferId);
this.offerDefs.put(item.getOfferId(), item.getId()); this.offerDefs.put(searchOfferId, item.getId());
} }
} else } else
item.update(set); item.update(set);
@@ -711,18 +712,22 @@ public class CatalogManager {
return; return;
} }
if (voucher.isExhausted()) { Voucher.ClaimResult claimResult = voucher.claimForUser(habbo.getHabboInfo().getId());
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR)); switch (claimResult) {
return; case CLAIMED:
break;
case EXHAUSTED:
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
return;
case USER_LIMIT:
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
return;
case FAILED:
default:
client.sendResponse(new RedeemVoucherErrorComposer(RedeemVoucherErrorComposer.TECHNICAL_ERROR));
return;
} }
if (voucher.hasUserExhausted(habbo.getHabboInfo().getId())) {
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
return;
}
voucher.addHistoryEntry(habbo.getHabboInfo().getId());
if (voucher.points > 0) { if (voucher.points > 0) {
client.getHabbo().givePoints(voucher.pointsType, voucher.points); client.getHabbo().givePoints(voucher.pointsType, voucher.points);
} }
@@ -1247,6 +1252,11 @@ public class CatalogManager {
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) { if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) {
if (baseItem.getName().equals("guild_forum") && guild.getOwnerId() != habbo.getHabboInfo().getId()) {
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata); InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata);
habboItem.setExtradata(""); habboItem.setExtradata("");
habboItem.needsUpdate(true); habboItem.needsUpdate(true);
@@ -14,6 +14,13 @@ import java.util.List;
public class Voucher { public class Voucher {
private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class); private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class);
public enum ClaimResult {
CLAIMED,
EXHAUSTED,
USER_LIMIT,
FAILED
}
public final int id; public final int id;
public final String code; public final String code;
public final int credits; public final int credits;
@@ -58,18 +65,34 @@ public class Voucher {
return this.amount > 0 && this.history.size() >= this.amount; return this.amount > 0 && this.history.size() >= this.amount;
} }
public void addHistoryEntry(int userId) { public synchronized ClaimResult claimForUser(int userId) {
int timestamp = Emulator.getIntUnixTimestamp(); if (this.isExhausted()) {
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp)); return ClaimResult.EXHAUSTED;
}
if (this.hasUserExhausted(userId)) {
return ClaimResult.USER_LIMIT;
}
int timestamp = Emulator.getIntUnixTimestamp();
if (!this.insertHistoryEntry(userId, timestamp)) {
return ClaimResult.FAILED;
}
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
return ClaimResult.CLAIMED;
}
private boolean insertHistoryEntry(int userId, int timestamp) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) {
statement.setInt(1, this.id); statement.setInt(1, this.id);
statement.setInt(2, userId); statement.setInt(2, userId);
statement.setInt(3, timestamp); statement.setInt(3, timestamp);
statement.execute(); return statement.executeUpdate() > 0;
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
return false;
} }
} }
} }
@@ -28,6 +28,8 @@ import java.util.List;
public class MarketPlace { public class MarketPlace {
private static final Logger LOGGER = LoggerFactory.getLogger(MarketPlace.class); private static final Logger LOGGER = LoggerFactory.getLogger(MarketPlace.class);
public static final int MINIMUM_LISTING_PRICE = 1;
public static final int MAXIMUM_LISTING_PRICE = 1_000_000_000;
//Configuration. Loaded from database & updated accordingly. //Configuration. Loaded from database & updated accordingly.
public static boolean MARKETPLACE_ENABLED = true; public static boolean MARKETPLACE_ENABLED = true;
@@ -56,6 +58,10 @@ public class MarketPlace {
public static void takeBackItem(Habbo habbo, int offerId) { public static void takeBackItem(Habbo habbo, int offerId) {
MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId); MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
if (offer == null) {
return;
}
if (!Emulator.getPluginManager().fireEvent(new MarketPlaceItemCancelledEvent(offer)).isCancelled()) { if (!Emulator.getPluginManager().fireEvent(new MarketPlaceItemCancelledEvent(offer)).isCancelled()) {
takeBackItem(habbo, offer); takeBackItem(habbo, offer);
} }
@@ -119,7 +125,7 @@ public class MarketPlace {
public static List<MarketPlaceOffer> getOffers(int minPrice, int maxPrice, String search, int sort) { public static List<MarketPlaceOffer> getOffers(int minPrice, int maxPrice, String search, int sort) {
List<MarketPlaceOffer> offers = new ArrayList<>(10); List<MarketPlaceOffer> offers = new ArrayList<>(10);
String query = "SELECT B.* FROM marketplace_items a INNER JOIN (SELECT b.item_id AS base_item_id, b.limited_data AS ltd_data, marketplace_items.*, AVG(price) as avg, MIN(marketplace_items.price) as minPrice, MAX(marketplace_items.price) as maxPrice, COUNT(*) as number, (SELECT COUNT(*) FROM marketplace_items c INNER JOIN items as items_b ON c.item_id = items_b.id WHERE state = 2 AND items_b.item_id = base_item_id AND DATE(from_unixtime(sold_timestamp)) = CURDATE()) as sold_count_today FROM marketplace_items INNER JOIN items b ON marketplace_items.item_id = b.id INNER JOIN items_base bi ON b.item_id = bi.id INNER JOIN catalog_items ci ON bi.id = ci.item_ids WHERE price = (SELECT MIN(e.price) FROM marketplace_items e, items d WHERE e.item_id = d.id AND d.item_id = b.item_id AND e.state = 1 AND e.timestamp > ? GROUP BY d.item_id) AND state = 1 AND timestamp > ?"; String query = "SELECT B.* FROM marketplace_items a INNER JOIN (SELECT b.item_id AS base_item_id, b.limited_data AS ltd_data, marketplace_items.*, AVG(price) as avg, MIN(marketplace_items.price) as minPrice, MAX(marketplace_items.price) as maxPrice, COUNT(*) as number, (SELECT COUNT(*) FROM marketplace_items c INNER JOIN items as items_b ON c.item_id = items_b.id WHERE state = 2 AND items_b.item_id = base_item_id AND DATE(from_unixtime(sold_timestamp)) = CURDATE()) as sold_count_today FROM marketplace_items INNER JOIN items b ON marketplace_items.item_id = b.id INNER JOIN items_base bi ON b.item_id = bi.id INNER JOIN catalog_items ci ON bi.id = ci.item_ids WHERE price = (SELECT MIN(e.price) FROM marketplace_items e, items d WHERE e.item_id = d.id AND d.item_id = b.item_id AND e.state = 1 AND e.timestamp > ? AND e.price BETWEEN ? AND ? GROUP BY d.item_id) AND state = 1 AND timestamp > ? AND marketplace_items.price BETWEEN ? AND ?";
if (minPrice > 0) { if (minPrice > 0) {
query += " AND CEIL(price + (price / 100)) >= ?"; query += " AND CEIL(price + (price / 100)) >= ?";
} }
@@ -163,7 +169,11 @@ public class MarketPlace {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) {
int paramIndex = 1; int paramIndex = 1;
statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800); statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800);
statement.setInt(paramIndex++, MINIMUM_LISTING_PRICE);
statement.setInt(paramIndex++, MAXIMUM_LISTING_PRICE);
statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800); statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800);
statement.setInt(paramIndex++, MINIMUM_LISTING_PRICE);
statement.setInt(paramIndex++, MAXIMUM_LISTING_PRICE);
if (minPrice > 0) { if (minPrice > 0) {
statement.setInt(paramIndex++, minPrice); statement.setInt(paramIndex++, minPrice);
} }
@@ -171,8 +181,9 @@ public class MarketPlace {
statement.setInt(paramIndex++, maxPrice); statement.setInt(paramIndex++, maxPrice);
} }
if (!search.isEmpty()) { if (!search.isEmpty()) {
statement.setString(paramIndex++, "%" + search + "%"); String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
statement.setString(paramIndex++, "%" + search + "%"); statement.setString(paramIndex++, likeSearch);
statement.setString(paramIndex++, likeSearch);
} }
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
@@ -264,7 +275,13 @@ public class MarketPlace {
itemSet.first(); itemSet.first();
if (itemSet.getRow() > 0) { if (itemSet.getRow() > 0) {
int price = MarketPlace.calculateCommision(set.getInt("price")); int rawPrice = set.getInt("price");
if (!isValidListingPrice(rawPrice)) {
sendErrorMessage(client, set.getInt("item_id"), offerId);
return;
}
int price = MarketPlace.calculateCommision(rawPrice);
if (set.getInt("state") != 1) { if (set.getInt("state") != 1) {
sendErrorMessage(client, set.getInt("item_id"), offerId); sendErrorMessage(client, set.getInt("item_id"), offerId);
} else if ((MARKETPLACE_CURRENCY == 0 && price > client.getHabbo().getHabboInfo().getCredits()) || (MARKETPLACE_CURRENCY > 0 && price > client.getHabbo().getHabboInfo().getCurrencyAmount(MARKETPLACE_CURRENCY))) { } else if ((MARKETPLACE_CURRENCY == 0 && price > client.getHabbo().getHabboInfo().getCredits()) || (MARKETPLACE_CURRENCY > 0 && price > client.getHabbo().getHabboInfo().getCurrencyAmount(MARKETPLACE_CURRENCY))) {
@@ -278,8 +295,9 @@ public class MarketPlace {
return; return;
} }
int soldTimestamp = Emulator.getIntUnixTimestamp();
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) { try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
updateOffer.setInt(1, Emulator.getIntUnixTimestamp()); updateOffer.setInt(1, soldTimestamp);
updateOffer.setInt(2, offerId); updateOffer.setInt(2, offerId);
int updated = updateOffer.executeUpdate(); int updated = updateOffer.executeUpdate();
if (updated == 0) { if (updated == 0) {
@@ -306,7 +324,11 @@ public class MarketPlace {
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price)); client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
if (habbo != null) { if (habbo != null) {
habbo.getInventory().getOffer(offerId).setState(MarketPlaceState.SOLD); MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
if (offer != null) {
offer.setState(MarketPlaceState.SOLD);
offer.setSoldTimestamp(soldTimestamp);
}
} }
} }
} }
@@ -352,7 +374,7 @@ public class MarketPlace {
if (item == null || client == null) if (item == null || client == null)
return false; return false;
if (!item.getBaseItem().allowMarketplace() || price < 0) if (!item.getBaseItem().allowMarketplace() || !isValidListingPrice(price))
return false; return false;
MarketPlaceItemOfferedEvent event = new MarketPlaceItemOfferedEvent(client.getHabbo(), item, price); MarketPlaceItemOfferedEvent event = new MarketPlaceItemOfferedEvent(client.getHabbo(), item, price);
@@ -368,6 +390,11 @@ public class MarketPlace {
event.item.setFromGift(false); event.item.setFromGift(false);
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo()); MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
if (!offer.isPersisted()) {
LOGGER.warn("Marketplace offer insert failed for user {} item {}", client.getHabbo().getHabboInfo().getId(), event.item.getId());
return false;
}
client.getHabbo().getInventory().addMarketplaceOffer(offer); client.getHabbo().getInventory().addMarketplaceOffer(offer);
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item); client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
item.setUserId(-1); item.setUserId(-1);
@@ -387,11 +414,12 @@ public class MarketPlace {
synchronized (client.getHabbo().getInventory()) { synchronized (client.getHabbo().getInventory()) {
for (MarketPlaceOffer offer : offers) { for (MarketPlaceOffer offer : offers) {
if (offer.getState().equals(MarketPlaceState.SOLD)) { if (offer.getState().equals(MarketPlaceState.SOLD)) {
client.getHabbo().getInventory().removeMarketplaceOffer(offer); if (removeUser(offer)) {
credits += offer.getPrice(); client.getHabbo().getInventory().removeMarketplaceOffer(offer);
removeUser(offer); credits += offer.getPrice();
offer.needsUpdate(true); offer.needsUpdate(true);
Emulator.getThreading().run(offer); Emulator.getThreading().run(offer);
}
} }
} }
} }
@@ -405,18 +433,24 @@ public class MarketPlace {
} }
} }
private static void removeUser(MarketPlaceOffer offer) { private static boolean removeUser(MarketPlaceOffer offer) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) {
statement.setInt(1, -1); statement.setInt(1, -1);
statement.setInt(2, offer.getOfferId()); statement.setInt(2, offer.getOfferId());
statement.execute(); return statement.executeUpdate() > 0;
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
return false;
} }
} }
public static int calculateCommision(int price) { public static int calculateCommision(int price) {
return price + (int) Math.ceil(price / 100.0); long commission = price + (long) Math.ceil(price / 100.0);
return commission > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) commission;
}
public static boolean isValidListingPrice(int price) {
return price >= MINIMUM_LISTING_PRICE && price <= MAXIMUM_LISTING_PRICE;
} }
} }
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
return this.offerId; return this.offerId;
} }
public boolean isPersisted() {
return this.offerId > 0;
}
public void setOfferId(int offerId) { public void setOfferId(int offerId) {
this.offerId = offerId; this.offerId = offerId;
} }
@@ -32,6 +32,11 @@ public class AlertCommand extends Command {
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUsername); Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUsername);
if (habbo != null) { if (habbo != null) {
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true;
}
habbo.alert(message + "\r\n -" + gameClient.getHabbo().getHabboInfo().getUsername()); habbo.alert(message + "\r\n -" + gameClient.getHabbo().getHabboInfo().getUsername());
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_alert.message_send").replace("%user%", targetUsername), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_alert.message_send").replace("%user%", targetUsername), RoomChatMessageBubbles.ALERT);
} else { } else {
@@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.HabboManager;
import java.util.List;
public class BanCommand extends Command { public class BanCommand extends Command {
public BanCommand() { public BanCommand() {
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";")); super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
@@ -58,7 +60,7 @@ public class BanCommand extends Command {
return true; return true;
} }
if (target.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
@@ -72,7 +74,13 @@ public class BanCommand extends Command {
} }
} }
ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1).get(0); List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1);
if (bans == null || bans.isEmpty()) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.user_offline"), RoomChatMessageBubbles.ALERT);
return true;
}
ModToolBan ban = bans.get(0);
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
return true; return true;
@@ -0,0 +1,46 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Rank;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
final class CommandTargetGuard {
private CommandTargetGuard() {
}
static boolean canTarget(Habbo moderator, Habbo target) {
return target != null && canTarget(moderator, target.getHabboInfo());
}
static boolean canTarget(Habbo moderator, HabboInfo target) {
if (moderator == null || target == null || moderator.getHabboInfo().getId() == target.getId()) {
return false;
}
int moderatorRankId = moderator.getHabboInfo().getRank().getId();
int targetRankId = target.getRank().getId();
return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId;
}
static boolean canAssignRank(Habbo moderator, Rank rank) {
if (moderator == null || rank == null) {
return false;
}
int moderatorRankId = moderator.getHabboInfo().getRank().getId();
int targetRankId = rank.getId();
return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId;
}
private static boolean isCoreRank(int rankId) {
int highestRankId = Emulator.getGameEnvironment().getPermissionsManager().getAllRanks().stream()
.mapToInt(Rank::getId)
.max()
.orElse(0);
return highestRankId > 0 && rankId >= highestRankId;
}
}
@@ -18,7 +18,7 @@ public class CommandsCommand extends Command {
for (Command c : commands) { for (Command c : commands) {
String textKey = "commands.description." + c.permission; String textKey = "commands.description." + c.permission;
String commandText = Emulator.getTexts().getValue(textKey, ""); String commandText = Emulator.getTexts().getValueQuietly(textKey, "");
String commandLine = ":" + c.keys[0]; String commandLine = ":" + c.keys[0];
String description = ""; String description = "";
@@ -29,7 +29,7 @@ public class DisconnectCommand extends Command {
return true; return true;
} }
if (target.getHabboInfo().getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_disconnect.higher_rank"), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_disconnect.higher_rank"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
@@ -47,6 +47,11 @@ public class GivePrefixCommand extends Command {
return true; return true;
} }
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true;
}
UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect); UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect);
prefix.run(); prefix.run();
target.getInventory().getPrefixesComponent().addPrefix(prefix); target.getInventory().getPrefixesComponent().addPrefix(prefix);
@@ -36,7 +36,7 @@ public class GiveRankCommand extends Command {
} }
if (rank != null) { if (rank != null) {
if (rank.getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canAssignRank(gameClient.getHabbo(), rank)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
@@ -44,7 +44,7 @@ public class GiveRankCommand extends Command {
HabboInfo habbo = HabboManager.getOfflineHabboInfo(params[1]); HabboInfo habbo = HabboManager.getOfflineHabboInfo(params[1]);
if (habbo != null) { if (habbo != null) {
if (habbo.getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher.other").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher.other").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
@@ -63,4 +63,4 @@ public class GiveRankCommand extends Command {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.errors.cmd_give_rank.not_found").replace("%id%", params[2]).replace("%username%", params[1]), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.errors.cmd_give_rank.not_found").replace("%id%", params[2]).replace("%username%", params[1]), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
} }
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.HabboManager;
import java.util.List;
public class IPBanCommand extends Command { public class IPBanCommand extends Command {
public final static int TEN_YEARS = 315569260; public final static int TEN_YEARS = 315569260;
@@ -45,17 +47,17 @@ public class IPBanCommand extends Command {
return true; return true;
} }
if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1); List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
count++; count += bans != null ? bans.size() : 0;
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) { for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
if (h != null) { if (h != null) {
count++; bans = Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1); count += bans != null ? bans.size() : 0;
} }
} }
} else { } else {
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.HabboManager;
import java.util.List;
public class MachineBanCommand extends Command { public class MachineBanCommand extends Command {
public MachineBanCommand() { public MachineBanCommand() {
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";")); super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
@@ -41,12 +43,13 @@ public class MachineBanCommand extends Command {
return true; return true;
} }
if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
count = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1).size(); List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1);
count = bans != null ? bans.size() : 0;
} else { } else {
@@ -58,4 +61,4 @@ public class MachineBanCommand extends Command {
return true; return true;
} }
} }
@@ -29,6 +29,11 @@ public class MuteCommand extends Command {
return true; return true;
} }
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true;
}
int duration = Integer.MAX_VALUE; int duration = Integer.MAX_VALUE;
if (params.length == 3) { if (params.length == 3) {
@@ -32,35 +32,44 @@ public class RedeemCommand extends Command {
for (HabboItem item : gameClient.getHabbo().getInventory().getItemsComponent().getItemsAsValueCollection()) { for (HabboItem item : gameClient.getHabbo().getInventory().getItemsComponent().getItemsAsValueCollection()) {
if (item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_") || item.getBaseItem().getName().startsWith("DF_") || item.getBaseItem().getName().startsWith("PF_")) { if (item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_") || item.getBaseItem().getName().startsWith("DF_") || item.getBaseItem().getName().startsWith("PF_")) {
if (item.getUserId() == gameClient.getHabbo().getHabboInfo().getId()) { if (item.getUserId() == gameClient.getHabbo().getHabboInfo().getId()) {
items.add(item); boolean redeemable = false;
if ((item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_")) && !item.getBaseItem().getName().contains("_diamond_")) { if ((item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_")) && !item.getBaseItem().getName().contains("_diamond_")) {
try { Integer amount = parsePositiveRedeemValue(item.getBaseItem().getName(), 1);
credits += Integer.parseInt(item.getBaseItem().getName().split("_")[1]); if (amount != null) {
} catch (Exception e) { Integer total = addRedeemValue(credits, amount);
if (total != null) {
credits = total;
redeemable = true;
}
} }
} else if (item.getBaseItem().getName().startsWith("PF_")) { } else if (item.getBaseItem().getName().startsWith("PF_")) {
try { Integer amount = parsePositiveRedeemValue(item.getBaseItem().getName(), 1);
pixels += Integer.parseInt(item.getBaseItem().getName().split("_")[1]); if (amount != null) {
} catch (Exception e) { Integer total = addRedeemValue(pixels, amount);
if (total != null) {
pixels = total;
redeemable = true;
}
} }
} else if (item.getBaseItem().getName().startsWith("DF_")) { } else if (item.getBaseItem().getName().startsWith("DF_")) {
int pointsType; Integer pointsType = parsePositiveRedeemValue(item.getBaseItem().getName(), 1);
int pointsAmount; Integer pointsAmount = parsePositiveRedeemValue(item.getBaseItem().getName(), 2);
pointsType = Integer.parseInt(item.getBaseItem().getName().split("_")[1]); if (pointsType != null && pointsAmount != null && addRedeemPoints(points, pointsType, pointsAmount)) {
pointsAmount = Integer.parseInt(item.getBaseItem().getName().split("_")[2]); redeemable = true;
}
points.adjustOrPutValue(pointsType, pointsAmount, pointsAmount);
} }
else if (item.getBaseItem().getName().startsWith("CF_diamond_")) { else if (item.getBaseItem().getName().startsWith("CF_diamond_")) {
int pointsType; Integer pointsAmount = parsePositiveRedeemValue(item.getBaseItem().getName(), 2);
int pointsAmount;
pointsType = 5; if (pointsAmount != null && addRedeemPoints(points, 5, pointsAmount)) {
pointsAmount = Integer.parseInt(item.getBaseItem().getName().split("_")[2]); redeemable = true;
}
}
points.adjustOrPutValue(pointsType, pointsAmount, pointsAmount); if (redeemable) {
items.add(item);
} }
} }
} }
@@ -103,4 +112,41 @@ public class RedeemCommand extends Command {
return true; return true;
} }
static Integer parsePositiveRedeemValue(String itemName, int index) {
if (itemName == null) {
return null;
}
String[] parts = itemName.split("_");
if (index < 0 || index >= parts.length) {
return null;
}
try {
int value = Integer.parseInt(parts[index]);
return value > 0 ? value : null;
} catch (NumberFormatException e) {
return null;
}
}
static Integer addRedeemValue(int current, int amount) {
try {
return Math.addExact(current, amount);
} catch (ArithmeticException e) {
return null;
}
}
static boolean addRedeemPoints(TIntIntMap points, int pointsType, int amount) {
int current = points.get(pointsType);
Integer total = addRedeemValue(current, amount);
if (total == null) {
return false;
}
points.put(pointsType, total);
return true;
}
} }
@@ -31,6 +31,11 @@ public class RemovePrefixCommand extends Command {
return true; return true;
} }
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true;
}
if (prefixIdStr.equalsIgnoreCase("all")) { if (prefixIdStr.equalsIgnoreCase("all")) {
List<UserPrefix> prefixes = target.getInventory().getPrefixesComponent().getPrefixes(); List<UserPrefix> prefixes = target.getInventory().getPrefixesComponent().getPrefixes();
for (UserPrefix prefix : prefixes) { for (UserPrefix prefix : prefixes) {
@@ -41,7 +41,7 @@ public class SuperbanCommand extends Command {
return true; return true;
} }
if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
@@ -56,4 +56,4 @@ public class SuperbanCommand extends Command {
return true; return true;
} }
} }
@@ -23,6 +23,11 @@ public class UnmuteCommand extends Command {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_unmute.not_found").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_unmute.not_found").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT);
return true; return true;
} else { } else {
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
return true;
}
if (!habbo.getHabboStats().allowTalk() || (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo))) { if (!habbo.getHabboStats().allowTalk() || (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo))) {
if (!habbo.getHabboStats().allowTalk()) { if (!habbo.getHabboStats().allowTalk()) {
habbo.unMute(); habbo.unMute();
@@ -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;
}
}
@@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class GameClient { public class GameClient {
@@ -24,6 +25,7 @@ public class GameClient {
private final LatencyTracker latencyTracker; private final LatencyTracker latencyTracker;
private Habbo habbo; private Habbo habbo;
private final AtomicBoolean disposed = new AtomicBoolean(false);
private boolean handshakeFinished; private boolean handshakeFinished;
private String machineId = ""; private String machineId = "";
private String ssoTicket = ""; private String ssoTicket = "";
@@ -149,6 +151,14 @@ public class GameClient {
} }
public void dispose() { public void dispose() {
this.dispose(true);
}
public void dispose(boolean allowSessionResume) {
if (!this.disposed.compareAndSet(false, true)) {
return;
}
try { try {
this.channel.close(); this.channel.close();
@@ -161,7 +171,7 @@ public class GameClient {
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect). // appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
if (this.habbo.getClient() == this && this.habbo.isOnline()) { if (this.habbo.getClient() == this && this.habbo.isOnline()) {
// Try to park the habbo in the grace period instead of immediate disconnect // Try to park the habbo in the grace period instead of immediate disconnect
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket); boolean parked = allowSessionResume && SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
if (!parked) { if (!parked) {
// No grace period configured — immediate disconnect as before // No grace period configured — immediate disconnect as before
@@ -177,4 +187,4 @@ public class GameClient {
LOGGER.error("Caught exception", e); LOGGER.error("Caught exception", e);
} }
} }
} }
@@ -43,14 +43,34 @@ public class GameClientManager {
public void disposeClient(GameClient client) { public void disposeClient(GameClient client) {
this.disposeClient(client.getChannel()); if (client == null) {
return;
}
this.disposeClient(client.getChannel(), true);
}
public void forceDisposeClient(GameClient client) {
if (client == null) {
return;
}
this.disposeClient(client.getChannel(), false);
} }
private void disposeClient(Channel channel) { private void disposeClient(Channel channel) {
this.disposeClient(channel, true);
}
private void disposeClient(Channel channel, boolean allowSessionResume) {
if (channel == null) {
return;
}
GameClient client = channel.attr(GameServerAttributes.CLIENT).get(); GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
if (client != null) { if (client != null) {
client.dispose(); client.dispose(allowSessionResume);
} }
channel.deregister(); channel.deregister();
channel.attr(GameServerAttributes.CLIENT).set(null); channel.attr(GameServerAttributes.CLIENT).set(null);
@@ -190,4 +210,4 @@ public class GameClientManager {
CFKeepAlive(); CFKeepAlive();
}, 30000); }, 30000);
} }
} }
@@ -71,6 +71,15 @@ public class SessionResumeManager {
} }
}, graceSeconds * 1000); }, graceSeconds * 1000);
if (future == null) {
// The scheduler refused the grace-expiry task (pool saturated or
// shutting down). Parking now would leave a GhostSession that nothing
// can ever reap (the Habbo + room refs pinned for the JVM lifetime),
// so disconnect immediately instead.
performFullDisconnect(habbo);
return false;
}
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd)); ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
applyPausedEffect(habbo); applyPausedEffect(habbo);
@@ -0,0 +1,52 @@
package com.eu.habbo.habbohotel.guilds;
import com.eu.habbo.messages.ClientMessage;
import com.eu.habbo.util.PacketGuard;
public final class GuildBadgeBuilder {
public static final int MAX_BADGE_PARTS = 5;
private static final int INTS_PER_PART = 3;
private static final int BYTES_PER_INT = 4;
private static final int MAX_PART_ID = 999;
private static final int MAX_COLOR_ID = 99;
private static final int MAX_POSITION = 8;
private GuildBadgeBuilder() {
}
public static String readBadge(ClientMessage packet, int flatPartValueCount) {
if (flatPartValueCount % INTS_PER_PART != 0) {
return null;
}
int partCount = flatPartValueCount / INTS_PER_PART;
if (!PacketGuard.isCountInRange(partCount, 1, MAX_BADGE_PARTS)
|| !PacketGuard.hasFixedWidthEntries(packet, flatPartValueCount, BYTES_PER_INT)) {
return null;
}
StringBuilder badge = new StringBuilder(partCount * 6);
for (int partIndex = 0; partIndex < partCount; partIndex++) {
int id = packet.readInt();
int color = packet.readInt();
int position = packet.readInt();
if (!isValidPart(id, color, position)) {
return null;
}
badge.append(partIndex == 0 ? "b" : "s");
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id);
badge.append(color < 10 ? "0" : "").append(color);
badge.append(position);
}
return badge.toString();
}
private static boolean isValidPart(int id, int color, int position) {
return id >= 0 && id <= MAX_PART_ID
&& color >= 0 && color <= MAX_COLOR_ID
&& position >= 0 && position <= MAX_POSITION;
}
}
@@ -291,11 +291,12 @@ public class GuildManager {
} }
} }
} else if (!error) { } else if (!error) {
try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ?")) { try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ? AND level_id = ?")) {
statement.setInt(1, GuildRank.MEMBER.type); statement.setInt(1, GuildRank.MEMBER.type);
statement.setInt(2, Emulator.getIntUnixTimestamp()); statement.setInt(2, Emulator.getIntUnixTimestamp());
statement.setInt(3, userId); statement.setInt(3, userId);
statement.setInt(4, guild.getId()); statement.setInt(4, guild.getId());
statement.setInt(5, GuildRank.REQUESTED.type);
statement.execute(); statement.execute();
} }
} }
@@ -421,9 +422,9 @@ public class GuildManager {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
statement.setInt(1, guild.getId()); statement.setInt(1, guild.getId());
statement.setString(2, "%" + query + "%"); statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
statement.setInt(3, page * 14); statement.setInt(3, page * 14);
statement.setInt(4, (page * 14) + 14); statement.setInt(4, 14);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
@@ -440,7 +441,7 @@ public class GuildManager {
public int getGuildMembersCount(Guild guild, int page, int levelId, String query) { public int getGuildMembersCount(Guild guild, int page, int levelId, String query) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC")) {
statement.setInt(1, guild.getId()); statement.setInt(1, guild.getId());
statement.setString(2, "%" + query + "%"); statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
@@ -101,21 +101,27 @@ public class ForumThread implements Runnable, ISerialize {
if (statement.executeUpdate() < 1) if (statement.executeUpdate() < 1)
return null; return null;
ResultSet set = statement.getGeneratedKeys(); try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) { if (set.next()) {
int threadId = set.getInt(1); int threadId = set.getInt(1);
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null); createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
cacheThread(createdThread); cacheThread(createdThread);
}
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
createdThread.addComment(comment);
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
} }
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
} }
// ForumThreadComment.create() opens its OWN connection; do it after the
// thread's connection has been released to avoid holding two pooled
// connections simultaneously per forum-thread creation.
if (createdThread != null) {
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
createdThread.addComment(comment);
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
}
return createdThread; return createdThread;
} }
@@ -98,12 +98,13 @@ public class ForumThreadComment implements Runnable, ISerialize {
if (statement.executeUpdate() < 1) if (statement.executeUpdate() < 1)
return null; return null;
ResultSet set = statement.getGeneratedKeys(); try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) { if (set.next()) {
int commentId = set.getInt(1); int commentId = set.getInt(1);
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0); createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment)); Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
}
} }
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -0,0 +1,52 @@
package com.eu.habbo.habbohotel.items;
/**
* Builds a complete furnidata entry object (single-line JSON5) from an {@link Item}
* (its items_base row) plus a display name/description. Used by the Furni Editor
* upsert path when a furni has no furnidata entry yet. Field shape mirrors the
* hotel's existing furnidata entries; {@code id} is the item's sprite id so the
* renderer resolves the furni's name/data by typeId.
*/
public final class FurnidataEntryBuilder {
private FurnidataEntryBuilder() {}
public static String build(Item item, String name, String description) {
String classname = item.getName() != null ? item.getName() : "";
String safeName = (name != null && !name.isBlank()) ? name
: (item.getFullName() != null && !item.getFullName().isBlank()) ? item.getFullName()
: classname;
String safeDesc = description != null ? description : "";
String customParams = item.getCustomParams() != null ? item.getCustomParams() : "";
StringBuilder b = new StringBuilder(256);
b.append("{\"id\":").append(item.getSpriteId());
b.append(",\"classname\":\"").append(esc(classname)).append('"');
b.append(",\"revision\":0,\"category\":\"unknown\",\"defaultdir\":0");
b.append(",\"xdim\":").append(item.getWidth());
b.append(",\"ydim\":").append(item.getLength());
b.append(",\"partcolors\":{\"color\":[]}");
b.append(",\"name\":\"").append(esc(safeName)).append('"');
b.append(",\"description\":\"").append(esc(safeDesc)).append('"');
b.append(",\"adurl\":\"\",\"offerid\":-1,\"buyout\":false,\"rentofferid\":-1,\"rentbuyout\":false,\"bc\":false,\"excludeddynamic\":false");
b.append(",\"customparams\":\"").append(esc(customParams)).append('"');
b.append(",\"specialtype\":1");
b.append(",\"canstandon\":").append(item.allowWalk());
b.append(",\"cansiton\":").append(item.allowSit());
b.append(",\"canlayon\":").append(item.allowLay());
b.append('}');
return b.toString();
}
/** Escape for a JSON string value; collapse control chars to spaces. */
private static String esc(String v) {
StringBuilder b = new StringBuilder(v.length() + 8);
for (int i = 0; i < v.length(); i++) {
char c = v.charAt(i);
if (c == '"' || c == '\\') b.append('\\').append(c);
else if (c == '\n' || c == '\r' || c == '\t') b.append(' ');
else b.append(c);
}
return b.toString();
}
}
@@ -36,30 +36,36 @@ public final class FurnidataSourceResolver {
public static Source resolve() { public static Source resolve() {
try { try {
String override = Emulator.getConfig().getValue("items.furnidata.path", ""); String override = Emulator.getConfig().getValue("items.furnidata.path", "");
if (!override.isEmpty()) {
Path p = Paths.get(override);
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path");
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path does not exist");
}
String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", ""); String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (!rendererConfigPath.isEmpty()) { return resolveConfigured(override, rendererConfigPath, assetBasePath);
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
}
Source fallback = resolveFromAssetBase(assetBasePath);
if (fallback != null) return fallback;
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("FurnidataSourceResolver failed", e); LOGGER.warn("FurnidataSourceResolver failed", e);
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "Resolver error"); return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "Resolver error");
} }
} }
public static Source resolveConfigured(String legacyOverridePath, String rendererConfigPath, String assetBasePath) {
if (rendererConfigPath != null && !rendererConfigPath.isEmpty()) {
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath == null || assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
}
Source fromAssetBase = resolveFromAssetBase(assetBasePath);
if (fromAssetBase != null && fromAssetBase.ok()) return fromAssetBase;
if (legacyOverridePath != null && !legacyOverridePath.isEmpty()) {
Path p = Paths.get(legacyOverridePath);
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path fallback");
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path fallback does not exist");
}
if (fromAssetBase != null) return fromAssetBase;
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
}
public static Source resolveFromRendererConfig(Path rendererConfig, Path assetBase) { public static Source resolveFromRendererConfig(Path rendererConfig, Path assetBase) {
try { try {
if (rendererConfig == null || !Files.exists(rendererConfig)) { if (rendererConfig == null || !Files.exists(rendererConfig)) {
@@ -115,32 +115,39 @@ public class FurnidataWatcher {
} }
} }
private void onChange() { private void onChange() throws InterruptedException {
// Re-index under the shared furnidata lock so the watcher and editor
// writes never swap the index concurrently. The lock is released before
// the throttle/broadcast below so a slow broadcast can't stall editor saves.
List<FurnidataEntry> delta;
FurnidataLock.LOCK.lock(); FurnidataLock.LOCK.lock();
try { try {
Path source = this.provider.getSource(); Path source = this.provider.getSource();
if (source == null) return; if (source == null) return;
delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
List<FurnidataEntry> delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
if (delta.isEmpty()) return;
long now = System.currentTimeMillis();
if (now - this.lastBroadcast < this.minIntervalMs) {
LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size());
return;
}
this.lastBroadcast = now;
FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
: new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
broadcast(composer);
LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
} finally { } finally {
FurnidataLock.LOCK.unlock(); FurnidataLock.LOCK.unlock();
} }
if (delta.isEmpty()) return;
// Min-interval throttle: the index has already been swapped, so we must
// not drop this delta (the next reindex would diff against the updated
// index and never re-emit it). Instead, defer the broadcast until the
// interval elapses. Running on a dedicated daemon thread, sleeping is
// safe; file events arriving meanwhile coalesce into the next cycle.
long sinceLast = System.currentTimeMillis() - this.lastBroadcast;
if (sinceLast < this.minIntervalMs) {
Thread.sleep(this.minIntervalMs - sinceLast);
}
this.lastBroadcast = System.currentTimeMillis();
FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
: new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
broadcast(composer);
LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
} }
private void broadcast(FurnitureDataReloadComposer composer) { private void broadcast(FurnitureDataReloadComposer composer) {
@@ -56,6 +56,98 @@ public class FurnidataWriter {
return true; return true;
} }
/** Outcome of a {@link #create} attempt. */
public enum CreateResult { CREATED, ALREADY_EXISTS, ID_COLLISION, NO_TARGET, IO_ERROR }
/**
* Append a brand-new furnidata entry (upsert's "create" half). Refuses if the
* classname already exists (caller should edit instead) or if {@code id} is
* already used by a DIFFERENT classname (id collision would break the
* {@code roomItem.name.<id>} / typeId resolution on the renderer). The complete
* entry object is built by the caller (see FurnidataEntryBuilder) and inserted
* right after the opening '[' of the matching section's "furnitype" array.
*
* @param classname new classname (must be absent from furnidata)
* @param id furnidata id (= item sprite id); must not collide
* @param type FLOOR -> roomitemtypes, WALL -> wallitemtypes
* @param entryJson5 the complete entry object as a single-line JSON5 string
* @param createTier split-tier only: the tier dir to write into (e.g. "custom"); ignored for single-file
*/
public CreateResult create(String classname, int id, FurnitureType type, String entryJson5, String createTier) {
String cn = classname == null ? "" : classname.trim().toLowerCase(java.util.Locale.ROOT);
if (cn.isEmpty() || entryJson5 == null || entryJson5.isBlank()) return CreateResult.NO_TARGET;
// Guard: duplicate classname / id collision (scan the whole source).
for (FurnidataEntry e : new FurnidataReader(source, maxBytes).read()) {
String ecn = e.classname() == null ? "" : e.classname().trim().toLowerCase(java.util.Locale.ROOT);
if (ecn.equals(cn)) return CreateResult.ALREADY_EXISTS;
if (e.id() == id) return CreateResult.ID_COLLISION;
}
try {
Path target = resolveCreateTarget(createTier);
if (target == null) return CreateResult.NO_TARGET;
String raw = Files.readString(target, StandardCharsets.UTF_8);
String section = (type == FurnitureType.WALL) ? "wallitemtypes" : "roomitemtypes";
int open = furnitypeArrayOpenIndex(raw, section);
if (open < 0) return CreateResult.NO_TARGET; // section/array absent in target file
String edited = raw.substring(0, open) + "\n" + entryJson5 + "," + raw.substring(open);
backup(target);
atomicWrite(target, edited);
return CreateResult.CREATED;
} catch (IOException e) {
return CreateResult.IO_ERROR;
}
}
/** Single-file: the source. Split-tier: the create-tier file (created with a shell if absent). */
private Path resolveCreateTarget(String createTier) throws IOException {
if (!directory) return source;
String tier = (createTier == null || createTier.isBlank()) ? "custom" : createTier.trim();
Path base = source.toAbsolutePath().normalize();
Path tierDir = safeResolve(base, tier);
if (tierDir == null) return null;
if (!Files.isDirectory(tierDir)) Files.createDirectories(tierDir);
for (String fileName : manifestList(tierDir, "files", List.of())) {
Path f = safeResolve(base, tierDir.resolve(fileName).toString());
if (f != null && Files.isRegularFile(f)) return f;
}
Path def = tierDir.resolve("furnidata.json5");
if (!Files.exists(def)) {
Files.writeString(def,
"{\n \"roomitemtypes\": { \"furnitype\": [\n] },\n \"wallitemtypes\": { \"furnitype\": [\n] }\n}\n",
StandardCharsets.UTF_8);
}
return def;
}
/** Index just after the '[' that opens {@code <section>.furnitype}, or -1 if absent. String-aware. */
static int furnitypeArrayOpenIndex(String raw, String section) {
int s = indexOfKey(raw, section, 0);
if (s < 0) return -1;
int ft = indexOfKey(raw, "furnitype", s);
if (ft < 0) return -1;
boolean inStr = false; char q = 0;
for (int i = ft; i < raw.length(); i++) {
char c = raw.charAt(i);
if (inStr) { if (c == '\\') i++; else if (c == q) inStr = false; continue; }
if (c == '"' || c == '\'') { inStr = true; q = c; }
else if (c == '[') return i + 1;
}
return -1;
}
/** First occurrence of a quoted key ("key" or 'key') at/after {@code from}, or -1. */
private static int indexOfKey(String raw, String key, int from) {
int a = raw.indexOf("\"" + key + "\"", from);
int b = raw.indexOf("'" + key + "'", from);
if (a < 0) return b;
if (b < 0) return a;
return Math.min(a, b);
}
/** For single-file just returns the file; for split-tier, the tier file that contains cn. */ /** For single-file just returns the file; for split-tier, the tier file that contains cn. */
private Path locateFile(String cn) throws IOException { private Path locateFile(String cn) throws IOException {
if (!directory) { if (!directory) {
@@ -27,6 +27,7 @@ public class FurnitureTextProvider {
private final boolean enabled; private final boolean enabled;
private volatile Map<String, FurniText> index = Map.of(); private volatile Map<String, FurniText> index = Map.of();
private volatile Path source; private volatile Path source;
private volatile String sourceDescription = "unknown";
private FurnidataWatcher watcher; private FurnidataWatcher watcher;
public FurnitureTextProvider(boolean enabled) { public FurnitureTextProvider(boolean enabled) {
@@ -47,7 +48,7 @@ public class FurnitureTextProvider {
return; return;
} }
reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read()); reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source); LOGGER.info("Furniture Text Provider -> Indexed! ({} names, source: {})", this.index.size(), this.sourceDescription);
if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) { if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) {
if (this.watcher != null) this.watcher.stop(); if (this.watcher != null) this.watcher.stop();
@@ -88,9 +89,12 @@ public class FurnitureTextProvider {
} }
} }
private static Path resolveSource() { private Path resolveSource() {
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve(); FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
if (source.ok()) return source.path(); if (source.ok()) {
this.sourceDescription = source.message();
return source.path();
}
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved ({}) - {}", source.status(), source.message()); LOGGER.warn("FurnitureTextProvider: no furnidata source resolved ({}) - {}", source.status(), source.message());
return null; return null;
} }
@@ -45,7 +45,7 @@ public class Item implements ISerialize {
} }
public static boolean isPet(Item item) { public static boolean isPet(Item item) {
return item.getName().toLowerCase().startsWith("a0 pet"); return item != null && item.getName() != null && item.getName().toLowerCase().startsWith("a0 pet");
} }
public static boolean isBot(Item item) { public static boolean isBot(Item item) {
@@ -121,26 +121,19 @@ public class Item implements ISerialize {
this.customParams = set.getString("customparams"); this.customParams = set.getString("customparams");
this.clothingOnWalk = set.getString("clothing_on_walk"); this.clothingOnWalk = set.getString("clothing_on_walk");
if (!set.getString("vending_ids").isEmpty()) { int[] vendingIds = ItemDataGuard.parsePositiveIntList(set.getString("vending_ids"));
if (vendingIds.length > 0) {
this.vendingItems = new TIntArrayList(); this.vendingItems = new TIntArrayList();
String[] vendingIds = set.getString("vending_ids").replace(";", ",").split(","); for (int vendingId : vendingIds) {
for (String s : vendingIds) { this.vendingItems.add(vendingId);
this.vendingItems.add(Integer.parseInt(s.replace(" ", "")));
} }
} else {
this.vendingItems = new TIntArrayList();
} }
//if(this.interactionType.getType() == InteractionMultiHeight.class || this.interactionType.getType().isAssignableFrom(InteractionMultiHeight.class)) //if(this.interactionType.getType() == InteractionMultiHeight.class || this.interactionType.getType().isAssignableFrom(InteractionMultiHeight.class))
{ {
if (set.getString("multiheight").contains(";")) { this.multiHeights = ItemDataGuard.parseHeights(set.getString("multiheight"));
String[] s = set.getString("multiheight").split(";");
this.multiHeights = new double[s.length];
for (int i = 0; i < s.length; i++) {
this.multiHeights[i] = Double.parseDouble(s[i]);
}
} else {
this.multiHeights = new double[0];
}
} }
this.rotations = 4; this.rotations = 4;
@@ -254,6 +247,10 @@ public class Item implements ISerialize {
} }
public int getRandomVendingItem() { public int getRandomVendingItem() {
if (this.vendingItems == null || this.vendingItems.isEmpty()) {
return 0;
}
return this.vendingItems.get(Emulator.getRandom().nextInt(this.vendingItems.size())); return this.vendingItems.get(Emulator.getRandom().nextInt(this.vendingItems.size()));
} }
@@ -273,21 +270,23 @@ public class Item implements ISerialize {
@Override @Override
public void serialize(ServerMessage message) { public void serialize(ServerMessage message) {
message.appendString(this.type.code.toLowerCase()); message.appendString(this.type == null ? "" : this.type.code.toLowerCase());
if (type == FurnitureType.BADGE) { if (type == FurnitureType.BADGE) {
message.appendString(this.customParams); message.appendString(ItemDataGuard.safeString(this.customParams));
} else { } else {
message.appendInt(this.spriteId); message.appendInt(this.spriteId);
if (this.getName().contains("wallpaper_single") || this.getName().contains("floor_single") || this.getName().contains("landscape_single")) { String itemName = ItemDataGuard.safeString(this.getName());
message.appendString(this.name.split("_")[2]); if (itemName.contains("wallpaper_single") || itemName.contains("floor_single") || itemName.contains("landscape_single")) {
String[] nameParts = itemName.split("_");
message.appendString(nameParts.length > 2 ? nameParts[2] : "");
} else if (type == FurnitureType.ROBOT) { } else if (type == FurnitureType.ROBOT) {
message.appendString(this.customParams); message.appendString(ItemDataGuard.safeString(this.customParams));
} else if (name.equalsIgnoreCase("poster")) { } else if (itemName.equalsIgnoreCase("poster")) {
message.appendString(this.customParams); message.appendString(ItemDataGuard.safeString(this.customParams));
} else if (name.startsWith("SONG ")) { } else if (itemName.startsWith("SONG ")) {
message.appendString(this.customParams); message.appendString(ItemDataGuard.safeString(this.customParams));
} else { } else {
message.appendString(""); message.appendString("");
} }
@@ -0,0 +1,82 @@
package com.eu.habbo.habbohotel.items;
final class ItemDataGuard {
static final int MAX_EXTRA_DATA_LENGTH = 1000;
private ItemDataGuard() {
}
static String safeString(String value) {
return value == null ? "" : value;
}
static String normalizeExtraData(String value) {
String safe = safeString(value);
return safe.length() > MAX_EXTRA_DATA_LENGTH ? safe.substring(0, MAX_EXTRA_DATA_LENGTH) : safe;
}
static int parsePositiveInt(String value) {
try {
int parsed = Integer.parseInt(safeString(value).trim());
return parsed > 0 ? parsed : 0;
} catch (NumberFormatException e) {
return 0;
}
}
static int[] parsePositiveIntList(String value) {
String safe = safeString(value).replace(";", ",").replace(".", ",");
if (safe.isBlank()) {
return new int[0];
}
String[] parts = safe.split(",");
int[] parsed = new int[parts.length];
int count = 0;
for (String part : parts) {
int id = parsePositiveInt(part);
if (id > 0) {
parsed[count++] = id;
}
}
if (count == parsed.length) {
return parsed;
}
int[] compact = new int[count];
System.arraycopy(parsed, 0, compact, 0, count);
return compact;
}
static double[] parseHeights(String value) {
String safe = safeString(value);
if (safe.isBlank() || !safe.contains(";")) {
return new double[0];
}
String[] parts = safe.split(";");
double[] parsed = new double[parts.length];
int count = 0;
for (String part : parts) {
try {
double height = Double.parseDouble(part.trim());
if (Double.isFinite(height)) {
parsed[count++] = height;
}
} catch (NumberFormatException e) {
// Ignore malformed DB values and keep the remaining heights usable.
}
}
if (count == parsed.length) {
return parsed;
}
double[] compact = new double[count];
System.arraycopy(parsed, 0, compact, 0, count);
return compact;
}
}
@@ -566,6 +566,10 @@ public class ItemManager {
public int calculateCrackState(int count, int max, Item baseItem) { public int calculateCrackState(int count, int max, Item baseItem) {
if (count <= 0 || max <= 0 || baseItem == null || baseItem.getStateCount() <= 0) {
return 0;
}
return (int) Math.floor((1.0D / ((double) max / (double) count) * baseItem.getStateCount())); return (int) Math.floor((1.0D / ((double) max / (double) count) * baseItem.getStateCount()));
} }
@@ -574,7 +578,8 @@ public class ItemManager {
} }
public Item getCrackableReward(int itemId) { public Item getCrackableReward(int itemId) {
return this.getItem(this.crackableRewards.get(itemId).getRandomReward()); CrackableReward reward = this.crackableRewards.get(itemId);
return reward == null ? null : this.getItem(reward.getRandomReward());
} }
@@ -604,6 +609,12 @@ public class ItemManager {
} }
public HabboItem createItem(int habboId, Item item, int limitedStack, int limitedSells, String extraData) { public HabboItem createItem(int habboId, Item item, int limitedStack, int limitedSells, String extraData) {
if (habboId <= 0 || item == null) {
return null;
}
extraData = ItemDataGuard.normalizeExtraData(extraData);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data, limited_data) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data, limited_data) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, habboId); statement.setInt(1, habboId);
statement.setInt(2, item.getId()); statement.setInt(2, item.getId());
@@ -673,6 +684,12 @@ public class ItemManager {
} }
public HabboItem handleRecycle(Habbo habbo, String itemId) { public HabboItem handleRecycle(Habbo habbo, String itemId) {
int rewardItemId = ItemDataGuard.parsePositiveInt(itemId);
if (habbo == null || habbo.getHabboInfo() == null || rewardItemId <= 0
|| Emulator.getGameEnvironment().getCatalogManager().ecotronItem == null) {
return null;
}
String extradata = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR); String extradata = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR);
HabboItem item = null; HabboItem item = null;
@@ -686,7 +703,7 @@ public class ItemManager {
try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents (item_id, base_item_reward) VALUES (?, ?)")) { try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents (item_id, base_item_reward) VALUES (?, ?)")) {
while (set.next() && item == null) { while (set.next() && item == null) {
preparedStatement.setInt(1, set.getInt(1)); preparedStatement.setInt(1, set.getInt(1));
preparedStatement.setInt(2, Integer.parseInt(itemId)); preparedStatement.setInt(2, rewardItemId);
preparedStatement.addBatch(); preparedStatement.addBatch();
item = new InteractionDefault(set.getInt(1), habbo.getHabboInfo().getId(), Emulator.getGameEnvironment().getCatalogManager().ecotronItem, extradata, 0, 0); item = new InteractionDefault(set.getInt(1), habbo.getHabboInfo().getId(), Emulator.getGameEnvironment().getCatalogManager().ecotronItem, extradata, 0, 0);
} }
@@ -829,6 +846,10 @@ public class ItemManager {
} }
public HabboItem createGift(String username, Item item, String extraData, int limitedStack, int limitedSells) { public HabboItem createGift(String username, Item item, String extraData, int limitedStack, int limitedSells) {
if (username == null || username.isBlank() || item == null) {
return null;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username); Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
int userId = 0; int userId = 0;
@@ -857,13 +878,13 @@ public class ItemManager {
} }
public HabboItem createGift(int userId, Item item, String extraData, int limitedStack, int limitedSells) { public HabboItem createGift(int userId, Item item, String extraData, int limitedStack, int limitedSells) {
if (userId == 0) if (userId <= 0 || item == null)
return null; return null;
if (extraData.length() > 1000) { if (extraData != null && extraData.length() > ItemDataGuard.MAX_EXTRA_DATA_LENGTH) {
LOGGER.error("Extradata exceeds maximum length of 1000 characters: {}", extraData); LOGGER.error("Extradata exceeds maximum length of 1000 characters: {}", extraData);
extraData = extraData.substring(0, 1000);
} }
extraData = ItemDataGuard.normalizeExtraData(extraData);
HabboItem gift = this.createItem(userId, item, limitedStack, limitedSells, extraData); HabboItem gift = this.createItem(userId, item, limitedStack, limitedSells, extraData);
@@ -879,7 +900,7 @@ public class ItemManager {
} }
public Item getItem(int itemId) { public Item getItem(int itemId) {
if (itemId < 0) if (itemId <= 0)
return null; return null;
return this.items.get(itemId); return this.items.get(itemId);
@@ -890,12 +911,16 @@ public class ItemManager {
} }
public Item getItem(String itemName) { public Item getItem(String itemName) {
if (itemName == null || itemName.isBlank()) {
return null;
}
TIntObjectIterator<Item> item = this.items.iterator(); TIntObjectIterator<Item> item = this.items.iterator();
for (int i = this.items.size(); i-- > 0; ) { for (int i = this.items.size(); i-- > 0; ) {
try { try {
item.advance(); item.advance();
if (item.value().getName().equalsIgnoreCase(itemName)) { if (item.value() != null && item.value().getName() != null && item.value().getName().equalsIgnoreCase(itemName)) {
return item.value(); return item.value();
} }
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
@@ -13,11 +13,13 @@ import org.slf4j.LoggerFactory;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.concurrent.atomic.AtomicBoolean;
public class InteractionGift extends HabboItem { public class InteractionGift extends HabboItem {
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class); private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
public boolean explode = false; public boolean explode = false;
private final AtomicBoolean opening = new AtomicBoolean(false);
private int[] itemId; private int[] itemId;
private int colorId = 0; private int colorId = 0;
private int ribbonId = 0; private int ribbonId = 0;
@@ -46,6 +48,15 @@ public class InteractionGift extends HabboItem {
} }
} }
/**
* Claims the right to open this gift, returning true exactly once. Guards
* against two near-simultaneous OpenRecycleBox packets both scheduling an
* (async, delayed) OpenGift before the wrapper is removed from the room.
*/
public boolean tryStartOpening() {
return this.opening.compareAndSet(false, true);
}
@Override @Override
public void serializeExtradata(ServerMessage serverMessage) { public void serializeExtradata(ServerMessage serverMessage) {
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId); //serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
@@ -65,9 +65,8 @@ public class InteractionMultiHeight extends HabboItem {
if (this.getBaseItem().getMultiHeights().length > 0) { if (this.getBaseItem().getMultiHeights().length > 0) {
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length)); this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
this.needsUpdate(true); this.needsUpdate(true);
room.updateTiles(room.getLayout().getTilesAt(room.getLayout().getTile(this.getX(), this.getY()), this.getBaseItem().getWidth(), this.getBaseItem().getLength(), this.getRotation())); room.updateItem(this);
room.updateItemState(this); this.updateUnitsOnItem(room);
//room.sendComposer(new UpdateStackHeightComposer(this.getX(), this.getY(), this.getBaseItem().getMultiHeights()[Integer.valueOf(this.getExtradata())] * 256.0D).compose());
} }
} }
} }
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.items.interactions;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomLayout; import com.eu.habbo.habbohotel.rooms.RoomLayout;
import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnit;
@@ -133,12 +134,18 @@ public class InteractionRentableSpace extends HabboItem {
if (habbo.getHabboStats().isRentingSpace()) if (habbo.getHabboStats().isRentingSpace())
return; return;
if (habbo.getHabboInfo().getCredits() < this.rentCost()) int cost = this.rentCost();
boolean hasInfiniteCredits = habbo.hasPermission(Permission.ACC_INFINITE_CREDITS);
if (!hasInfiniteCredits && habbo.getHabboInfo().getCredits() < cost)
return; return;
if (habbo.getHabboStats().getClubExpireTimestamp() < Emulator.getIntUnixTimestamp()) if (habbo.getHabboStats().getClubExpireTimestamp() < Emulator.getIntUnixTimestamp())
return; return;
if (!hasInfiniteCredits) {
habbo.giveCredits(-cost);
}
this.setRenterId(habbo.getHabboInfo().getId()); this.setRenterId(habbo.getHabboInfo().getId());
this.setRenterName(habbo.getHabboInfo().getUsername()); this.setRenterName(habbo.getHabboInfo().getUsername());
this.setEndTimestamp(Emulator.getIntUnixTimestamp() + (7 * 86400)); this.setEndTimestamp(Emulator.getIntUnixTimestamp() + (7 * 86400));
@@ -2,6 +2,7 @@ package com.eu.habbo.habbohotel.items.interactions;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredInputGuard;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnit;
@@ -18,6 +19,7 @@ import java.sql.SQLException;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras). * Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
@@ -61,7 +63,11 @@ public abstract class InteractionWired extends InteractionDefault {
*/ */
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000; private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
private long cooldown; private volatile long cooldown;
// Ensures one box is processed by a single thread at a time, so the
// cooldown check-and-set in WiredHandler can't double-fire when a packet
// thread and the room cycle thread trigger the same box concurrently.
private final AtomicBoolean processing = new AtomicBoolean(false);
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
InteractionWired(ResultSet set, Item baseItem) throws SQLException { InteractionWired(ResultSet set, Item baseItem) throws SQLException {
@@ -149,6 +155,15 @@ public abstract class InteractionWired extends InteractionDefault {
this.cooldown = newMillis; this.cooldown = newMillis;
} }
/** Claims exclusive processing of this box; returns false if another thread is already in it. */
public boolean tryBeginProcessing() {
return this.processing.compareAndSet(false, true);
}
public void endProcessing() {
this.processing.set(false);
}
@Override @Override
public boolean allowWiredResetState() { public boolean allowWiredResetState() {
return false; return false;
@@ -216,39 +231,18 @@ public abstract class InteractionWired extends InteractionDefault {
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect) public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
{ {
int intParamCount = packet.readInt(); int[] intParams = WiredInputGuard.readIntParams(packet);
if (intParamCount < 0 || intParamCount > 100) { String stringParam = WiredInputGuard.readStringParam(packet);
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount); int[] itemIds = WiredInputGuard.readFurniIds(packet);
}
int[] intParams = new int[intParamCount];
for(int i = 0; i < intParamCount; i++)
{
intParams[i] = packet.readInt();
}
String stringParam = packet.readString();
int itemCount = packet.readInt();
int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5;
if (itemCount < 0 || itemCount > selectionLimit * 20) {
throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit");
}
int[] itemIds = new int[itemCount];
for(int i = 0; i < itemCount; i++)
{
itemIds[i] = packet.readInt();
}
WiredSettings settings = new WiredSettings(intParams, stringParam, itemIds, -1); WiredSettings settings = new WiredSettings(intParams, stringParam, itemIds, -1);
if(isEffect) if(isEffect)
{ {
settings.setDelay(packet.readInt()); settings.setDelay(WiredInputGuard.normalizeDelay(packet.readInt()));
} }
settings.setStuffTypeSelectionCode(packet.readInt()); settings.setStuffTypeSelectionCode(WiredInputGuard.normalizeStuffSelectionCode(packet.readInt()));
return settings; return settings;
} }
} }
@@ -190,6 +190,14 @@ public class InteractionPetBreedingNest extends HabboItem {
} }
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) { public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
// Guard before the destructive delete below: a crafted packet can call
// this on a nest that isn't full, which would delete the nest furni and
// then NPE on petOne/petTwo in the async runnable (losing the furni).
if (habbo == null || this.petOne == null || this.petTwo == null
|| habbo.getHabboInfo().getCurrentRoom() == null) {
return;
}
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId())); Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
this.setExtradata("2"); this.setExtradata("2");
@@ -0,0 +1,90 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.ClientMessage;
import java.util.Arrays;
public final class WiredInputGuard {
public static final int MAX_INT_PARAMS = 100;
public static final int MAX_STRING_PARAM_LENGTH = 1024;
public static final int MAX_ABSOLUTE_FURNI_IDS = 100;
public static final int DEFAULT_MAX_DELAY = 20;
public static final int MAX_ABSOLUTE_DELAY = 3600;
public static final int MIN_STUFF_SELECTION_CODE = -1;
public static final int MAX_STUFF_SELECTION_CODE = 2;
private WiredInputGuard() {
}
public static int[] readIntParams(ClientMessage packet) {
int count = packet.readInt();
if (count < 0 || count > MAX_INT_PARAMS) {
throw new IllegalArgumentException("Invalid wired int param count");
}
int[] values = new int[count];
for (int i = 0; i < count; i++) {
values[i] = packet.readInt();
}
return values;
}
public static String readStringParam(ClientMessage packet) {
String value = packet.readString();
if (value == null || value.isEmpty()) {
return "";
}
return value.length() > MAX_STRING_PARAM_LENGTH
? value.substring(0, MAX_STRING_PARAM_LENGTH)
: value;
}
public static int[] readFurniIds(ClientMessage packet) {
int count = packet.readInt();
int maxCount = maxFurniSelectionCount();
if (count < 0 || count > maxCount) {
throw new IllegalArgumentException("Invalid wired furni selection count");
}
int[] values = new int[count];
int accepted = 0;
for (int i = 0; i < count; i++) {
int itemId = packet.readInt();
if (itemId > 0) {
values[accepted++] = itemId;
}
}
return accepted == values.length ? values : Arrays.copyOf(values, accepted);
}
public static int normalizeDelay(int delay) {
return Math.max(0, Math.min(delay, maxDelay()));
}
public static int normalizeStuffSelectionCode(int code) {
if (code < MIN_STUFF_SELECTION_CODE || code > MAX_STUFF_SELECTION_CODE) {
return MIN_STUFF_SELECTION_CODE;
}
return code;
}
public static int maxFurniSelectionCount() {
int selectionLimit = Emulator.getConfig() != null
? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5)
: 5;
selectionLimit = Math.max(1, selectionLimit);
return Math.min(MAX_ABSOLUTE_FURNI_IDS, selectionLimit * 20);
}
public static int maxDelay() {
int configured = Emulator.getConfig() != null
? Emulator.getConfig().getInt("hotel.wired.max_delay", DEFAULT_MAX_DELAY)
: DEFAULT_MAX_DELAY;
configured = Math.max(0, configured);
return Math.min(MAX_ABSOLUTE_DELAY, configured);
}
}
@@ -0,0 +1,48 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.HabboItem;
import java.util.ArrayList;
import java.util.List;
public final class WiredLegacyDataGuard {
public static final int DEFAULT_MAX_DELAY = 20;
private WiredLegacyDataGuard() {
}
public static int parseDelay(String value) {
try {
int parsed = Integer.parseInt(value == null ? "" : value.trim());
return Math.max(0, Math.min(parsed, DEFAULT_MAX_DELAY));
} catch (NumberFormatException e) {
return 0;
}
}
public static List<HabboItem> parseRoomItems(String value, Room room) {
List<HabboItem> items = new ArrayList<>();
if (room == null || value == null || value.isBlank()) {
return items;
}
for (String part : value.split(";")) {
try {
int itemId = Integer.parseInt(part.trim());
if (itemId <= 0) {
continue;
}
HabboItem item = room.getHabboItem(itemId);
if (item != null) {
items.add(item);
}
} catch (NumberFormatException e) {
// Ignore malformed legacy ids and keep loading the remaining items.
}
}
return items;
}
}
@@ -0,0 +1,38 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import com.eu.habbo.Emulator;
public final class WiredNumericInputGuard {
public static final int DEFAULT_MAX_REWARD_AMOUNT = 1000;
public static final int DEFAULT_MAX_RESPECT_AMOUNT = 100;
public static final int MAX_ABSOLUTE_AMOUNT = 100000;
private WiredNumericInputGuard() {
}
public static int parsePositiveAmount(String value, int maxAmount) {
try {
int parsed = Integer.parseInt(value == null ? "" : value.trim());
if (parsed <= 0) {
return 0;
}
return Math.min(parsed, Math.max(1, Math.min(maxAmount, MAX_ABSOLUTE_AMOUNT)));
} catch (NumberFormatException e) {
return 0;
}
}
public static int maxRewardAmount() {
return configuredMax("hotel.wired.reward.max_amount", DEFAULT_MAX_REWARD_AMOUNT);
}
public static int maxRespectAmount() {
return configuredMax("hotel.wired.respect.max_amount", DEFAULT_MAX_RESPECT_AMOUNT);
}
private static int configuredMax(String key, int fallback) {
int configured = Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback;
return Math.max(1, Math.min(configured, MAX_ABSOLUTE_AMOUNT));
}
}
@@ -0,0 +1,45 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
public final class WiredTimerInputGuard {
public static final int MAX_TIMER_MS = 24 * 60 * 60 * 1000;
private WiredTimerInputGuard() {
}
public static int fromClientUnits(int units, int stepMs, int minMs) {
return fromClientUnits(units, stepMs, minMs, MAX_TIMER_MS);
}
public static int fromClientUnits(int units, int stepMs, int minMs, int maxMs) {
if (units < 1 || stepMs < 1) {
return minMs;
}
long value = (long) units * stepMs;
return clamp(value, minMs, maxMs);
}
public static int normalizeStoredMillis(Integer millis, int minMs, int fallbackMs) {
return normalizeStoredMillis(millis, minMs, fallbackMs, MAX_TIMER_MS);
}
public static int normalizeStoredMillis(Integer millis, int minMs, int fallbackMs, int maxMs) {
if (millis == null || millis < minMs) {
return fallbackMs;
}
return clamp(millis.longValue(), minMs, maxMs);
}
private static int clamp(long value, int minMs, int maxMs) {
if (value < minMs) {
return minMs;
}
if (value > maxMs) {
return maxMs;
}
return (int) value;
}
}
@@ -111,7 +111,13 @@ public class WiredConditionActorDir extends InteractionWiredCondition {
} }
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.onPickUp();
return;
}
if (data == null) { if (data == null) {
return; return;
@@ -157,15 +163,15 @@ public class WiredConditionActorDir extends InteractionWiredCondition {
return (this.directionMask & (1 << direction)) != 0; return (this.directionMask & (1 << direction)) != 0;
} }
private int normalizeDirectionMask(int value) { int normalizeDirectionMask(int value) {
return value & ALL_DIRECTIONS_MASK; return value & ALL_DIRECTIONS_MASK;
} }
private int normalizeUserSource(int value) { int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
} }
private int normalizeQuantifier(int value) { int normalizeQuantifier(int value) {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
@@ -111,19 +111,21 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.items.clear(); this.resetSettings();
this.comparison = COMPARISON_EQUAL;
this.minutes = 0;
this.halfSecondSteps = 0;
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ALL;
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) {
return; return;
} }
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.resetSettings();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -131,7 +133,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
this.comparison = this.normalizeComparison(data.comparison); this.comparison = this.normalizeComparison(data.comparison);
this.minutes = this.normalizeMinutes(data.minutes); this.minutes = this.normalizeMinutes(data.minutes);
this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps); this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps);
this.furniSource = data.furniSource; this.furniSource = this.normalizeFurniSource(data.furniSource);
this.quantifier = this.normalizeQuantifier(data.quantifier); this.quantifier = this.normalizeQuantifier(data.quantifier);
if (data.itemIds == null) { if (data.itemIds == null) {
@@ -139,6 +141,10 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
} }
for (Integer id : data.itemIds) { for (Integer id : data.itemIds) {
if (id == null) {
continue;
}
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item instanceof InteractionGameUpCounter) { if (item instanceof InteractionGameUpCounter) {
this.items.add(item); this.items.add(item);
@@ -195,7 +201,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
this.minutes = (params.length > 1) ? this.normalizeMinutes(params[1]) : 0; this.minutes = (params.length > 1) ? this.normalizeMinutes(params[1]) : 0;
this.halfSecondSteps = (params.length > 2) ? this.normalizeHalfSecondSteps(params[2]) : 0; this.halfSecondSteps = (params.length > 2) ? this.normalizeHalfSecondSteps(params[2]) : 0;
this.furniSource = (params.length > 3) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER; this.furniSource = (params.length > 3) ? this.normalizeFurniSource(params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 4) ? this.normalizeQuantifier(params[4]) : QUANTIFIER_ALL; this.quantifier = (params.length > 4) ? this.normalizeQuantifier(params[4]) : QUANTIFIER_ALL;
this.items.clear(); this.items.clear();
@@ -224,6 +230,15 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
return true; return true;
} }
private void resetSettings() {
this.items.clear();
this.comparison = COMPARISON_EQUAL;
this.minutes = 0;
this.halfSecondSteps = 0;
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ALL;
}
private void refresh(Room room) { private void refresh(Room room) {
THashSet<HabboItem> remove = new THashSet<>(); THashSet<HabboItem> remove = new THashSet<>();
@@ -256,7 +271,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
} }
} }
private int normalizeComparison(int value) { int normalizeComparison(int value) {
if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { if (value < COMPARISON_LESS || value > COMPARISON_GREATER) {
return COMPARISON_EQUAL; return COMPARISON_EQUAL;
} }
@@ -264,18 +279,30 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
return value; return value;
} }
private int normalizeMinutes(int value) { int normalizeMinutes(int value) {
return Math.max(0, Math.min(MAX_MINUTES, value)); return Math.max(0, Math.min(MAX_MINUTES, value));
} }
private int normalizeHalfSecondSteps(int value) { int normalizeHalfSecondSteps(int value) {
return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value)); return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value));
} }
private int normalizeQuantifier(int value) { int normalizeQuantifier(int value) {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
int normalizeFurniSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
static class JsonData { static class JsonData {
int comparison; int comparison;
int minutes; int minutes;
@@ -53,8 +53,7 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 2) return false; if(settings.getIntParams().length < 2) return false;
this.startDate = settings.getIntParams()[0]; this.applyRange(settings.getIntParams()[0], settings.getIntParams()[1]);
this.endDate = settings.getIntParams()[1];
return true; return true;
} }
@@ -80,20 +79,28 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
this.applyRange(0, 0);
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.startDate = data.startDate; if (data == null) {
this.endDate = data.endDate; this.applyRange(0, 0);
return;
}
this.applyRange(data.startDate, data.endDate);
} else { } else {
String[] data = wiredData.split("\t"); String[] data = wiredData.split("\t");
if (data.length == 2) { if (data.length == 2) {
try { try {
this.startDate = Integer.parseInt(data[0]); this.applyRange(Integer.parseInt(data[0]), Integer.parseInt(data[1]));
this.endDate = Integer.parseInt(data[1]);
} catch (Exception e) { } catch (Exception e) {
this.applyRange(0, 0);
} }
} }
} }
@@ -105,6 +112,12 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition {
this.endDate = 0; this.endDate = 0;
} }
private void applyRange(int startDate, int endDate) {
int[] range = WiredDateRangeInputGuard.normalizeRange(startDate, endDate);
this.startDate = range[0];
this.endDate = range[1];
}
static class JsonData { static class JsonData {
int startDate; int startDate;
int endDate; int endDate;
@@ -92,21 +92,35 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
this.all = data.all; try {
this.furniSource = data.furniSource; data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.onPickUp();
return;
}
for(int id : data.itemIds) { if (data == null) {
return;
}
this.all = data.all;
this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item != null) { if (item != null) {
this.items.add(item); this.items.add(item);
} }
} }
} else { } else {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
@@ -114,21 +128,18 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
this.all = (data[0].equals("1")); this.all = (data[0].equals("1"));
if (data.length == 2) { if (data.length == 2) {
String[] items = data[1].split(";"); for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id);
for (String s : items) { if (item != null) {
HabboItem item = room.getHabboItem(Integer.parseInt(s));
if (item != null)
this.items.add(item); this.items.add(item);
}
} }
} }
} }
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
} }
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
} }
@Override @Override
@@ -172,14 +183,12 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.all = params[0] == 1; this.all = params[0] == 1;
this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
int count = settings.getFurniIds().length; int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear(); this.items.clear();
@@ -198,6 +207,18 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
return true; return true;
} }
int normalizeFurniSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
private void refresh() { private void refresh() {
THashSet<HabboItem> items = new THashSet<>(); THashSet<HabboItem> items = new THashSet<>();
@@ -89,15 +89,18 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.items.clear(); this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.furniSource = data.furniSource; this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
this.all = data.all; this.all = data.all;
for(int id : data.itemIds) { for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item != null) { if (item != null) {
@@ -107,23 +110,19 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
} else { } else {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
if (data.length >= 1) { if (data.length >= 2) {
for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id);
String[] items = data[1].split(";"); if (item != null) {
for (String s : items) {
HabboItem item = room.getHabboItem(Integer.parseInt(s));
if (item != null)
this.items.add(item); this.items.add(item);
}
} }
} }
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
this.all = false; this.all = false;
} }
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
} }
@Override @Override
@@ -162,11 +161,9 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.all = (params.length > 0) && (params[0] == 1); this.all = (params.length > 0) && (params[0] == 1);
this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER);
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear(); this.items.clear();
@@ -186,6 +183,18 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
return true; return true;
} }
int normalizeFurniSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection<Habbo> habbos, Collection<Bot> bots, Collection<Pet> pets) { protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection<Habbo> habbos, Collection<Bot> bots, Collection<Pet> pets) {
RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY()); RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY());
if (baseTile == null) return false; if (baseTile == null) return false;
@@ -52,6 +52,10 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null) {
return false;
}
if (this.quantifier == QUANTIFIER_ANY) { if (this.quantifier == QUANTIFIER_ANY) {
return this.evaluateAnyMatches(ctx); return this.evaluateAnyMatches(ctx);
} }
@@ -158,7 +162,14 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
} }
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.onPickUp();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -310,8 +321,8 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
} }
} }
private void loadItems(Room room, List<Integer> itemIds, THashSet<HabboItem> target) { void loadItems(Room room, List<Integer> itemIds, THashSet<HabboItem> target) {
if (itemIds == null) { if (room == null || itemIds == null || target == null) {
return; return;
} }
@@ -335,7 +346,7 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
.collect(Collectors.joining(";")); .collect(Collectors.joining(";"));
} }
private List<Integer> parseIds(String value) { List<Integer> parseIds(String value) {
List<Integer> result = new ArrayList<>(); List<Integer> result = new ArrayList<>();
if (value == null || value.isEmpty()) { if (value == null || value.isEmpty()) {
return result; return result;
@@ -16,6 +16,7 @@ import java.sql.SQLException;
public class WiredConditionHabboCount extends InteractionWiredCondition { public class WiredConditionHabboCount extends InteractionWiredCondition {
public static final WiredConditionType type = WiredConditionType.USER_COUNT; public static final WiredConditionType type = WiredConditionType.USER_COUNT;
static final int MAX_USER_COUNT_LIMIT = 1000;
private int lowerLimit = 0; private int lowerLimit = 0;
private int upperLimit = 50; private int upperLimit = 50;
@@ -31,6 +32,10 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER)
? ctx.room().getUserCount() ? ctx.room().getUserCount()
: WiredSourceUtil.resolveUsers(ctx, this.userSource).size(); : WiredSourceUtil.resolveUsers(ctx, this.userSource).size();
@@ -55,20 +60,29 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.lowerLimit = data.lowerLimit; this.applyLimits(data.lowerLimit, data.upperLimit);
this.upperLimit = data.upperLimit; this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource);
this.userSource = data.userSource;
} else { } else {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
this.lowerLimit = Integer.parseInt(data[0]); if (data.length >= 2) {
this.upperLimit = Integer.parseInt(data[1]); try {
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.applyLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim()));
} catch (NumberFormatException ignored) {
// malformed legacy data keep the constructed defaults
}
}
} }
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
} }
@Override @Override
@@ -104,14 +118,19 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 2) return false; if(settings.getIntParams().length < 2) return false;
this.lowerLimit = settings.getIntParams()[0];
this.upperLimit = settings.getIntParams()[1];
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER; this.applyLimits(params[0], params[1]);
this.userSource = (params.length > 2) ? WiredConditionInputGuard.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER;
return true; return true;
} }
private void applyLimits(int lowerLimit, int upperLimit) {
int[] limits = WiredConditionInputGuard.normalizeUserCountRange(lowerLimit, upperLimit);
this.lowerLimit = limits[0];
this.upperLimit = limits[1];
}
static class JsonData { static class JsonData {
int lowerLimit; int lowerLimit;
int upperLimit; int upperLimit;
@@ -18,6 +18,7 @@ import java.util.List;
public class WiredConditionHabboHasEffect extends InteractionWiredCondition { public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ALL = 0;
protected static final int QUANTIFIER_ANY = 1; protected static final int QUANTIFIER_ANY = 1;
protected static final int MAX_EFFECT_ID = 10_000;
public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_EFFECT; public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_EFFECT;
@@ -86,15 +87,34 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
this.onPickUp();
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
this.effectId = data.effectId; try {
this.userSource = data.userSource; data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException ignored) {
this.onPickUp();
return;
}
if (data == null) {
this.onPickUp();
return;
}
this.effectId = WiredUserConditionInputGuard.normalizeEffectId(data.effectId);
this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource);
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
} else { } else {
this.effectId = Integer.parseInt(wiredData); try {
this.effectId = WiredUserConditionInputGuard.normalizeEffectId(Integer.parseInt(wiredData));
} catch (NumberFormatException ignored) {
this.effectId = 0;
}
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ANY; this.quantifier = QUANTIFIER_ANY;
} }
@@ -134,8 +154,8 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 1) return false; if(settings.getIntParams().length < 1) return false;
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.effectId = params[0]; this.effectId = WiredUserConditionInputGuard.normalizeEffectId(params[0]);
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 1) ? WiredUserConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY;
return true; return true;
@@ -153,6 +173,14 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
protected int normalizeEffectId(int value) {
return Math.max(0, Math.min(MAX_EFFECT_ID, value));
}
protected int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
static class JsonData { static class JsonData {
int effectId; int effectId;
int userSource; int userSource;
@@ -10,17 +10,15 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredContext;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.ServerMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHabboHasHandItem.class);
protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ALL = 0;
protected static final int QUANTIFIER_ANY = 1; protected static final int QUANTIFIER_ANY = 1;
protected static final int MAX_HAND_ITEM_ID = 10_000;
public static final WiredConditionType type = WiredConditionType.ACTOR_HAS_HANDITEM; public static final WiredConditionType type = WiredConditionType.ACTOR_HAS_HANDITEM;
@@ -62,9 +60,9 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 1) return false; if(settings.getIntParams().length < 1) return false;
this.handItem = this.normalizeHandItem(settings.getIntParams()[0]); this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(settings.getIntParams()[0]);
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 1) ? WiredUserConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
return true; return true;
@@ -99,21 +97,35 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
try { String wiredData = set.getString("wired_data");
String wiredData = set.getString("wired_data"); if (wiredData == null || wiredData.isEmpty()) {
this.onPickUp();
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
this.handItem = this.normalizeHandItem(data.handItemId); try {
this.userSource = data.userSource; data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.quantifier = this.normalizeQuantifier(data.quantifier); } catch (RuntimeException ignored) {
} else { this.onPickUp();
this.handItem = this.normalizeHandItem(Integer.parseInt(wiredData)); return;
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ALL;
} }
} catch (Exception e) { if (data == null) {
LOGGER.error("Caught exception", e); this.onPickUp();
return;
}
this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(data.handItemId);
this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource);
this.quantifier = this.normalizeQuantifier(data.quantifier);
} else {
try {
this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(Integer.parseInt(wiredData));
} catch (NumberFormatException ignored) {
this.handItem = 0;
}
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ALL;
} }
} }
@@ -156,14 +168,14 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
return true; return true;
} }
protected int normalizeHandItem(int value) {
return Math.max(0, value);
}
protected int normalizeQuantifier(int value) { protected int normalizeQuantifier(int value) {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
protected int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
static class JsonData { static class JsonData {
int handItemId; int handItemId;
int userSource; int userSource;
@@ -20,6 +20,7 @@ import java.util.List;
public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ALL = 0;
protected static final int QUANTIFIER_ANY = 1; protected static final int QUANTIFIER_ANY = 1;
protected static final int MAX_BADGE_CODE_LENGTH = 64;
public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_BADGE; public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_BADGE;
@@ -37,6 +38,10 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
Room room = ctx.room(); Room room = ctx.room();
List<RoomUnit> targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); List<RoomUnit> targets = WiredSourceUtil.resolveUsers(ctx, this.userSource);
if (targets.isEmpty()) return false; if (targets.isEmpty()) return false;
@@ -102,15 +107,30 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null) {
this.onPickUp();
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
this.badge = data.badge; try {
this.userSource = data.userSource; data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException ignored) {
this.onPickUp();
return;
}
if (data == null) {
this.onPickUp();
return;
}
this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(data.badge);
this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource);
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
} else { } else {
this.badge = wiredData; this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(wiredData);
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ANY; this.quantifier = QUANTIFIER_ANY;
} }
@@ -147,9 +167,9 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
this.badge = settings.getStringParam(); this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(settings.getStringParam());
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 0) ? WiredUserConditionInputGuard.normalizeUserSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY; this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY;
return true; return true;
@@ -167,6 +187,19 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
protected String normalizeBadge(String value) {
if (value == null) {
return "";
}
String normalized = value.trim();
return normalized.length() <= MAX_BADGE_CODE_LENGTH ? normalized : normalized.substring(0, MAX_BADGE_CODE_LENGTH);
}
protected int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
static class JsonData { static class JsonData {
String badge; String badge;
int userSource; int userSource;
@@ -97,7 +97,14 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
return; return;
} }
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.onPickUp();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -112,6 +119,10 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
} }
for (Integer id : data.itemIds) { for (Integer id : data.itemIds) {
if (id == null) {
continue;
}
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item != null) { if (item != null) {
this.items.add(item); this.items.add(item);
@@ -225,7 +236,7 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
} }
} }
private int normalizeComparison(int value) { int normalizeComparison(int value) {
if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { if (value < COMPARISON_LESS || value > COMPARISON_GREATER) {
return COMPARISON_EQUAL; return COMPARISON_EQUAL;
} }
@@ -233,11 +244,11 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
return value; return value;
} }
private int normalizeQuantifier(int value) { int normalizeQuantifier(int value) {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
private int normalizeFurniSource(int value) { int normalizeFurniSource(int value) {
switch (value) { switch (value) {
case WiredSourceUtil.SOURCE_SELECTED: case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR: case WiredSourceUtil.SOURCE_SELECTOR:
@@ -249,12 +260,12 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
} }
} }
private double normalizeAltitude(double value) { double normalizeAltitude(double value) {
double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value));
return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue();
} }
private double parseAltitudeOrDefault(String value) { double parseAltitudeOrDefault(String value) {
if (value == null || value.trim().isEmpty()) { if (value == null || value.trim().isEmpty()) {
return 0.0D; return 0.0D;
} }
@@ -266,7 +277,7 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
} }
} }
private String formatAltitude(double value) { String formatAltitude(double value) {
BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros(); BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros();
return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString(); return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString();
} }
@@ -0,0 +1,57 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.games.GameTeamColors;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
public final class WiredConditionInputGuard {
public static final int MAX_USER_COUNT_LIMIT = 1000;
public static final int MAX_TIMER_CYCLES = 24 * 60 * 60 * 2;
private WiredConditionInputGuard() {
}
public static GameTeamColors normalizeTeamColor(GameTeamColors value, GameTeamColors fallback) {
return (value != null) ? value : fallback;
}
public static GameTeamColors normalizeTeamColorType(int value, GameTeamColors fallback) {
for (GameTeamColors color : GameTeamColors.values()) {
if (color.type == value) {
return color;
}
}
return fallback;
}
public static int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
public static int normalizeTimerCycles(int value) {
if (value < 0) {
return 0;
}
return Math.min(value, MAX_TIMER_CYCLES);
}
public static int[] normalizeUserCountRange(int lowerLimit, int upperLimit) {
int lower = clampUserCount(lowerLimit);
int upper = clampUserCount(upperLimit);
if (lower > upper) {
return new int[]{upper, lower};
}
return new int[]{lower, upper};
}
private static int clampUserCount(int value) {
if (value < 0) {
return 0;
}
return Math.min(value, MAX_USER_COUNT_LIMIT);
}
}
@@ -52,12 +52,13 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition {
try { try {
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.cycles = data.cycles; this.cycles = WiredConditionInputGuard.normalizeTimerCycles(data.cycles);
} else { } else {
if (!wiredData.equals("")) if (!wiredData.equals(""))
this.cycles = Integer.parseInt(wiredData); this.cycles = WiredConditionInputGuard.normalizeTimerCycles(Integer.parseInt(wiredData));
} }
} catch (Exception e) { } catch (Exception e) {
this.cycles = 0;
} }
} }
@@ -90,10 +91,14 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 1) return false; if(settings.getIntParams().length < 1) return false;
this.cycles = settings.getIntParams()[0]; this.cycles = WiredConditionInputGuard.normalizeTimerCycles(settings.getIntParams()[0]);
return true; return true;
} }
int normalizeCycles(int value) {
return Math.max(0, Math.min(WiredConditionMoreTimeElapsed.MAX_CYCLES, value));
}
static class JsonData { static class JsonData {
int cycles; int cycles;
@@ -125,7 +125,14 @@ public class WiredConditionMatchDate extends InteractionWiredCondition {
} }
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.reset();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -193,7 +200,7 @@ public class WiredConditionMatchDate extends InteractionWiredCondition {
} }
} }
private int normalizeMode(int value) { int normalizeMode(int value) {
if (value < MODE_SKIP || value > MODE_RANGE) { if (value < MODE_SKIP || value > MODE_RANGE) {
return MODE_SKIP; return MODE_SKIP;
} }
@@ -201,20 +208,20 @@ public class WiredConditionMatchDate extends InteractionWiredCondition {
return value; return value;
} }
private int normalizeDay(int value) { int normalizeDay(int value) {
return Math.max(1, Math.min(31, value)); return Math.max(1, Math.min(31, value));
} }
private int normalizeYear(int value) { int normalizeYear(int value) {
return Math.max(1, Math.min(9999, value)); return Math.max(1, Math.min(9999, value));
} }
private int normalizeWeekdayMask(int value) { int normalizeWeekdayMask(int value) {
int normalized = value & ALL_WEEKDAYS_MASK; int normalized = value & ALL_WEEKDAYS_MASK;
return (normalized == 0) ? ALL_WEEKDAYS_MASK : normalized; return (normalized == 0) ? ALL_WEEKDAYS_MASK : normalized;
} }
private int normalizeMonthMask(int value) { int normalizeMonthMask(int value) {
int normalized = value & ALL_MONTHS_MASK; int normalized = value & ALL_MONTHS_MASK;
return (normalized == 0) ? ALL_MONTHS_MASK : normalized; return (normalized == 0) ? ALL_MONTHS_MASK : normalized;
} }
@@ -87,7 +87,7 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
this.direction = params[1] == 1; this.direction = params[1] == 1;
this.position = params[2] == 1; this.position = params[2] == 1;
this.altitude = (params.length > 3) && (params[3] == 1); this.altitude = (params.length > 3) && (params[3] == 1);
this.furniSource = (params.length > 4) ? params[4] : ((params.length > 3 && params[3] > 1) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER); this.furniSource = (params.length > 4) ? WiredMatchPositionInputGuard.normalizeFurniSource(params[4], false) : ((params.length > 3 && params[3] > 1) ? WiredMatchPositionInputGuard.normalizeFurniSource(params[3], false) : WiredSourceUtil.SOURCE_TRIGGER);
this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL; this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL;
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
@@ -108,11 +108,17 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY(), item.getZ())); this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY(), item.getZ()));
} }
this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(this.furniSource, !this.settings.isEmpty());
return true; return true;
} }
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
this.refresh(); this.refresh();
if (this.settings.isEmpty()) if (this.settings.isEmpty())
@@ -126,6 +132,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
} }
protected boolean evaluateAllTargetsMatch(WiredContext ctx) { protected boolean evaluateAllTargetsMatch(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
Room room = ctx.room(); Room room = ctx.room();
if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) {
@@ -159,6 +169,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
} }
protected boolean evaluateAnyTargetMatches(WiredContext ctx) { protected boolean evaluateAnyTargetMatches(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
Room room = ctx.room(); Room room = ctx.room();
if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) {
@@ -247,44 +261,84 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.onPickUp();
return;
}
if (data == null) {
return;
}
this.state = data.state; this.state = data.state;
this.position = data.position; this.position = data.position;
this.direction = data.direction; this.direction = data.direction;
this.altitude = data.altitude; this.altitude = data.altitude;
if (data.settings != null) { this.settings.addAll(WiredMatchPositionInputGuard.sanitizeSettings(data.settings, room));
this.settings.addAll(data.settings); this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(data.furniSource, !this.settings.isEmpty());
}
this.furniSource = data.furniSource;
this.quantifier = this.normalizeQuantifier(data.quantifier); this.quantifier = this.normalizeQuantifier(data.quantifier);
} else { } else {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
int itemCount = Integer.parseInt(data[0]); if (data.length >= 5) {
try {
int itemCount = Math.min(Integer.parseInt(data[0]), WiredManager.MAXIMUM_FURNI_SELECTION);
String[] items = data[1].split(";"); String[] items = data[1].split(";");
for (int i = 0; i < itemCount; i++) { for (int i = 0; i < itemCount && i < items.length; i++) {
String[] stuff = items[i].split("-"); WiredMatchFurniSetting setting = this.parseLegacySetting(items[i], room);
if (setting != null) {
this.settings.add(setting);
}
}
if (stuff.length >= 6) this.state = data[2].equals("1");
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5]))); this.direction = data[3].equals("1");
else if (stuff.length >= 5) this.position = data[4].equals("1");
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]))); } catch (NumberFormatException ignored) {
// malformed legacy data keep whatever was parsed plus defaults
}
} }
this.state = data[2].equals("1");
this.direction = data[3].equals("1");
this.position = data[4].equals("1");
this.altitude = false; this.altitude = false;
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_TRIGGER, !this.settings.isEmpty());
this.quantifier = QUANTIFIER_ALL; this.quantifier = QUANTIFIER_ALL;
} }
} }
private WiredMatchFurniSetting parseLegacySetting(String value, Room room) {
String[] parts = value.split("-", 6);
if (parts.length < 5) {
return null;
}
try {
double z = (parts.length >= 6) ? Double.parseDouble(parts[5]) : 0.0D;
return WiredMatchPositionInputGuard.sanitizeParts(
Integer.parseInt(parts[0]),
parts[1],
Integer.parseInt(parts[2]),
Integer.parseInt(parts[3]),
Integer.parseInt(parts[4]),
z,
room
);
} catch (NumberFormatException ignored) {
return null;
}
}
@Override @Override
public void onPickUp() { public void onPickUp() {
this.settings.clear(); this.settings.clear();
@@ -296,10 +350,58 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
this.quantifier = QUANTIFIER_ALL; this.quantifier = QUANTIFIER_ALL;
} }
private int normalizeQuantifier(int value) { int normalizeQuantifier(int value) {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
int normalizeFurniSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_TRIGGER:
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
WiredMatchFurniSetting normalizeSetting(WiredMatchFurniSetting setting) {
if (setting == null || setting.item_id <= 0) {
return null;
}
int rotation = Math.max(0, Math.min(7, setting.rotation));
int x = Math.max(0, setting.x);
int y = Math.max(0, setting.y);
double z = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, setting.z));
return new WiredMatchFurniSetting(setting.item_id, setting.state, rotation, x, y, z);
}
WiredMatchFurniSetting parseLegacySetting(String[] values) {
if (values == null || values.length < 5) {
return null;
}
try {
int itemId = Integer.parseInt(values[0]);
if (itemId <= 0) {
return null;
}
String state = values[1];
int rotation = Integer.parseInt(values[2]);
int x = Integer.parseInt(values[3]);
int y = Integer.parseInt(values[4]);
double z = values.length >= 6 ? Double.parseDouble(values[5]) : 0.0D;
return this.normalizeSetting(new WiredMatchFurniSetting(itemId, state, rotation, x, y, z));
} catch (RuntimeException exception) {
return null;
}
}
protected void refresh() { protected void refresh() {
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
@@ -126,7 +126,14 @@ public class WiredConditionMatchTime extends InteractionWiredCondition {
} }
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.reset();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -195,7 +202,7 @@ public class WiredConditionMatchTime extends InteractionWiredCondition {
} }
} }
private int normalizeMode(int value) { int normalizeMode(int value) {
if (value < MODE_SKIP || value > MODE_RANGE) { if (value < MODE_SKIP || value > MODE_RANGE) {
return MODE_SKIP; return MODE_SKIP;
} }
@@ -203,11 +210,11 @@ public class WiredConditionMatchTime extends InteractionWiredCondition {
return value; return value;
} }
private int normalizeHour(int value) { int normalizeHour(int value) {
return Math.max(0, Math.min(23, value)); return Math.max(0, Math.min(23, value));
} }
private int normalizeMinuteOrSecond(int value) { int normalizeMinuteOrSecond(int value) {
return Math.max(0, Math.min(59, value)); return Math.max(0, Math.min(59, value));
} }
@@ -16,6 +16,7 @@ import java.sql.SQLException;
public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition { public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
private static final WiredConditionType type = WiredConditionType.TIME_MORE_THAN; private static final WiredConditionType type = WiredConditionType.TIME_MORE_THAN;
static final int MAX_CYCLES = 1_000_000;
private int cycles; private int cycles;
@@ -52,12 +53,13 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
try { try {
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.cycles = data.cycles; this.cycles = WiredConditionInputGuard.normalizeTimerCycles(data.cycles);
} else { } else {
if (!wiredData.equals("")) if (!wiredData.equals(""))
this.cycles = Integer.parseInt(wiredData); this.cycles = WiredConditionInputGuard.normalizeTimerCycles(Integer.parseInt(wiredData));
} }
} catch (Exception e) { } catch (Exception e) {
this.cycles = 0;
} }
} }
@@ -90,10 +92,14 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 1) return false; if(settings.getIntParams().length < 1) return false;
this.cycles = settings.getIntParams()[0]; this.cycles = WiredConditionInputGuard.normalizeTimerCycles(settings.getIntParams()[0]);
return true; return true;
} }
int normalizeCycles(int value) {
return Math.max(0, Math.min(MAX_CYCLES, value));
}
static class JsonData { static class JsonData {
int cycles; int cycles;
@@ -99,9 +99,9 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.all = data.all; this.all = data.all;
this.furniSource = data.furniSource; this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
for (int id : data.itemIds) { for (int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item != null) { if (item != null) {
@@ -115,10 +115,8 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
this.all = (data[0].equals("1")); this.all = (data[0].equals("1"));
if (data.length == 2) { if (data.length == 2) {
String[] items = data[1].split(";"); for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id);
for (String s : items) {
HabboItem item = room.getHabboItem(Integer.parseInt(s));
if (item != null) if (item != null)
this.items.add(item); this.items.add(item);
@@ -127,9 +125,7 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
} }
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
} }
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
} }
@Override @Override
@@ -172,14 +168,12 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
if(settings.getIntParams().length < 1) return false; if(settings.getIntParams().length < 1) return false;
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.all = params[0] == 1; this.all = params[0] == 1;
this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
int count = settings.getFurniIds().length; int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear(); this.items.clear();
@@ -95,10 +95,10 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
WiredConditionFurniHaveHabbo.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniHaveHabbo.JsonData.class); WiredConditionFurniHaveHabbo.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniHaveHabbo.JsonData.class);
this.furniSource = data.furniSource; this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
this.all = data.all; this.all = data.all;
for(int id : data.itemIds) { for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item != null) { if (item != null) {
@@ -108,11 +108,9 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
} else { } else {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
if (data.length >= 1) { if (data.length >= 2) {
String[] items = data[1].split(";"); for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id);
for (String s : items) {
HabboItem item = room.getHabboItem(Integer.parseInt(s));
if (item != null) if (item != null)
this.items.add(item); this.items.add(item);
@@ -121,9 +119,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
this.all = false; this.all = false;
} }
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
} }
@Override @Override
@@ -161,11 +157,9 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.all = (params.length > 0) && (params[0] == 1); this.all = (params.length > 0) && (params[0] == 1);
this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER);
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear(); this.items.clear();
@@ -20,6 +20,10 @@ public class WiredConditionNotFurniTypeMatch extends WiredConditionFurniTypeMatc
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null) {
return false;
}
if (this.getQuantifier() == QUANTIFIER_ANY) { if (this.getQuantifier() == QUANTIFIER_ANY) {
return !this.evaluateAllMatches(ctx); return !this.evaluateAllMatches(ctx);
} }
@@ -6,8 +6,8 @@ import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.wired.WiredConditionType; import com.eu.habbo.habbohotel.wired.WiredConditionType;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredContext;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.ServerMessage;
@@ -31,6 +31,10 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER)
? ctx.room().getUserCount() ? ctx.room().getUserCount()
: WiredSourceUtil.resolveUsers(ctx, this.userSource).size(); : WiredSourceUtil.resolveUsers(ctx, this.userSource).size();
@@ -55,25 +59,34 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
WiredConditionHabboCount.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class); WiredConditionHabboCount.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class);
this.lowerLimit = data.lowerLimit; this.applyLimits(data.lowerLimit, data.upperLimit);
this.upperLimit = data.upperLimit; this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource);
this.userSource = data.userSource;
} else { } else {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
this.lowerLimit = Integer.parseInt(data[0]); if (data.length >= 2) {
this.upperLimit = Integer.parseInt(data[1]); try {
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.applyLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim()));
} catch (NumberFormatException ignored) {
// malformed legacy data keep the constructed defaults
}
}
} }
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
} }
@Override @Override
public void onPickUp() { public void onPickUp() {
this.upperLimit = 0; this.lowerLimit = 10;
this.lowerLimit = 20; this.upperLimit = 20;
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
} }
@@ -103,14 +116,19 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 2) return false; if(settings.getIntParams().length < 2) return false;
this.lowerLimit = settings.getIntParams()[0];
this.upperLimit = settings.getIntParams()[1];
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER; this.applyLimits(params[0], params[1]);
this.userSource = (params.length > 2) ? WiredConditionInputGuard.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER;
return true; return true;
} }
private void applyLimits(int lowerLimit, int upperLimit) {
int[] limits = WiredConditionInputGuard.normalizeUserCountRange(lowerLimit, upperLimit);
this.lowerLimit = limits[0];
this.upperLimit = limits[1];
}
static class JsonData { static class JsonData {
int lowerLimit; int lowerLimit;
int upperLimit; int upperLimit;
@@ -22,6 +22,10 @@ public class WiredConditionNotMatchStatePosition extends WiredConditionMatchStat
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
this.refresh(); this.refresh();
if (this.getMatchFurniSettings().isEmpty()) { if (this.getMatchFurniSettings().isEmpty()) {
@@ -29,6 +29,7 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition {
protected static final int COMPARISON_EQUAL = 1; protected static final int COMPARISON_EQUAL = 1;
protected static final int COMPARISON_HIGHER = 2; protected static final int COMPARISON_HIGHER = 2;
protected static final int TEAM_TRIGGERER = 0; protected static final int TEAM_TRIGGERER = 0;
protected static final int MAX_SCORE = 1_000_000;
private static final GameTeamColors[] SUPPORTED_TEAM_COLORS = new GameTeamColors[] { private static final GameTeamColors[] SUPPORTED_TEAM_COLORS = new GameTeamColors[] {
GameTeamColors.RED, GameTeamColors.RED,
@@ -96,7 +97,11 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition {
} }
protected int normalizeScore(int value) { protected int normalizeScore(int value) {
return Math.max(0, value); if (value < 0) {
return 0;
}
return Math.min(value, MAX_SCORE);
} }
protected int normalizeExplicitTeamType(int value) { protected int normalizeExplicitTeamType(int value) {
@@ -65,7 +65,13 @@ public class WiredConditionTeamHasRank extends WiredConditionTeamGameBase {
return; return;
} }
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException ignored) {
this.resetSettings();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -66,7 +66,13 @@ public class WiredConditionTeamHasScore extends WiredConditionTeamGameBase {
return; return;
} }
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException ignored) {
this.resetSettings();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -97,12 +97,12 @@ public class WiredConditionTeamMember extends InteractionWiredCondition {
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.teamColor = data.teamColor; this.teamColor = WiredConditionInputGuard.normalizeTeamColor(data.teamColor, GameTeamColors.RED);
this.userSource = data.userSource; this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource);
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
} else { } else {
if (!wiredData.equals("")) if (!wiredData.equals(""))
this.teamColor = GameTeamColors.values()[Integer.parseInt(wiredData)]; this.teamColor = WiredConditionInputGuard.normalizeTeamColorType(Integer.parseInt(wiredData), GameTeamColors.RED);
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ANY; this.quantifier = QUANTIFIER_ANY;
} }
@@ -147,8 +147,8 @@ public class WiredConditionTeamMember extends InteractionWiredCondition {
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if(settings.getIntParams().length < 1) return false; if(settings.getIntParams().length < 1) return false;
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.teamColor = GameTeamColors.values()[params[0]]; this.teamColor = WiredConditionInputGuard.normalizeTeamColorType(params[0], GameTeamColors.RED);
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 1) ? WiredConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ALL) : QUANTIFIER_ANY; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ALL) : QUANTIFIER_ANY;
return true; return true;
@@ -42,6 +42,10 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null || ctx.room() == null) {
return false;
}
this.refresh(); this.refresh();
List<RoomUnit> userTargets = WiredSourceUtil.resolveUsers(ctx, this.userSource); List<RoomUnit> userTargets = WiredSourceUtil.resolveUsers(ctx, this.userSource);
@@ -104,16 +108,19 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
@Override @Override
public void loadWiredData(ResultSet set, Room room) throws SQLException { public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.items.clear(); this.onPickUp();
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) { if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.furniSource = data.furniSource; this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
this.userSource = data.userSource; this.userSource = WiredFurniConditionInputGuard.normalizeUserSource(data.userSource);
this.quantifier = this.normalizeQuantifier(data.quantifier); this.quantifier = this.normalizeQuantifier(data.quantifier);
for(int id : data.itemIds) { for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id); HabboItem item = room.getHabboItem(id);
if (item != null) { if (item != null) {
@@ -121,10 +128,8 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
} }
} }
} else { } else {
String[] data = wiredData.split(";"); for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(wiredData, WiredManager.MAXIMUM_FURNI_SELECTION)) {
HabboItem item = room.getHabboItem(id);
for (String s : data) {
HabboItem item = room.getHabboItem(Integer.parseInt(s));
if (item != null) { if (item != null) {
this.items.add(item); this.items.add(item);
@@ -134,9 +139,7 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = QUANTIFIER_ALL; this.quantifier = QUANTIFIER_ALL;
} }
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
} }
@Override @Override
@@ -182,13 +185,11 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
int[] params = settings.getIntParams(); int[] params = settings.getIntParams();
this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; this.furniSource = (params.length > 0) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER;
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear(); this.items.clear();
@@ -233,6 +234,22 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
int normalizeFurniSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
@Override @Override
public WiredConditionOperator operator() { public WiredConditionOperator operator() {
return WiredConditionOperator.AND; return WiredConditionOperator.AND;
@@ -33,6 +33,7 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ALL = 0;
protected static final int QUANTIFIER_ANY = 1; protected static final int QUANTIFIER_ANY = 1;
protected static final int SOURCE_SPECIFIED_USERNAME = 101; protected static final int SOURCE_SPECIFIED_USERNAME = 101;
protected static final int MAX_USERNAME_LENGTH = 64;
public static final WiredConditionType type = WiredConditionType.TRIGGERER_MATCH; public static final WiredConditionType type = WiredConditionType.TRIGGERER_MATCH;
@@ -84,7 +85,14 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
return; return;
} }
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.resetSettings();
return;
}
if (data == null) { if (data == null) {
return; return;
} }
@@ -284,7 +292,7 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
return ""; return "";
} }
private int normalizeEntityType(int value) { int normalizeEntityType(int value) {
switch (value) { switch (value) {
case ENTITY_HABBO: case ENTITY_HABBO:
case ENTITY_PET: case ENTITY_PET:
@@ -295,19 +303,19 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
} }
} }
private int normalizeAvatarMode(int value) { int normalizeAvatarMode(int value) {
return (value == AVATAR_MODE_CERTAIN) ? AVATAR_MODE_CERTAIN : AVATAR_MODE_ANY; return (value == AVATAR_MODE_CERTAIN) ? AVATAR_MODE_CERTAIN : AVATAR_MODE_ANY;
} }
private int normalizeQuantifier(int value) { int normalizeQuantifier(int value) {
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
} }
private int normalizePrimaryUserSource(int value) { int normalizePrimaryUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
} }
private int normalizeCompareUserSource(int value) { int normalizeCompareUserSource(int value) {
switch (value) { switch (value) {
case WiredSourceUtil.SOURCE_CLICKED_USER: case WiredSourceUtil.SOURCE_CLICKED_USER:
case SOURCE_SPECIFIED_USERNAME: case SOURCE_SPECIFIED_USERNAME:
@@ -317,8 +325,13 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
} }
} }
private String normalizeUsername(String value) { String normalizeUsername(String value) {
return (value == null) ? "" : value.trim(); if (value == null) {
return "";
}
String normalized = value.trim();
return normalized.length() <= MAX_USERNAME_LENGTH ? normalized : normalized.substring(0, MAX_USERNAME_LENGTH);
} }
protected static class MatchResult { protected static class MatchResult {
@@ -87,7 +87,13 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition
return; return;
} }
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException ignored) {
this.resetSettings();
return;
}
if (data == null) { if (data == null) {
return; return;
@@ -253,7 +259,7 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition
} }
long timestamp = (Long) timestampValue; long timestamp = (Long) timestampValue;
if ((System.currentTimeMillis() - timestamp) > TRANSIENT_ACTION_WINDOW_MS) { if (!WiredUserActionInputGuard.isRecentTimestamp(timestamp, System.currentTimeMillis(), TRANSIENT_ACTION_WINDOW_MS)) {
return false; return false;
} }
@@ -35,6 +35,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
private static final int DURATION_UNIT_WEEKS = 5; private static final int DURATION_UNIT_WEEKS = 5;
private static final int DURATION_UNIT_MONTHS = 6; private static final int DURATION_UNIT_MONTHS = 6;
private static final int DURATION_UNIT_YEARS = 7; private static final int DURATION_UNIT_YEARS = 7;
static final int MAX_DURATION_AMOUNT = 1_000_000;
protected int compareValue = COMPARE_VALUE_CREATED; protected int compareValue = COMPARE_VALUE_CREATED;
protected int comparison = COMPARISON_LOWER_THAN; protected int comparison = COMPARISON_LOWER_THAN;
@@ -97,7 +98,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER; this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER;
this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED; this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED;
this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN; this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN;
this.durationAmount = Math.max(0, (params.length > 3) ? params[3] : 0); this.durationAmount = normalizeDurationAmount((params.length > 3) ? params[3] : 0);
this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS; this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS;
this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER;
this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER; this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER;
@@ -130,6 +131,10 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null) {
return false;
}
Room room = ctx.room(); Room room = ctx.room();
if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) { if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) {
@@ -192,7 +197,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
this.targetType = normalizeTargetTypeExtended(data.targetType); this.targetType = normalizeTargetTypeExtended(data.targetType);
this.compareValue = normalizeCompareValue(data.compareValue); this.compareValue = normalizeCompareValue(data.compareValue);
this.comparison = normalizeComparison(data.comparison); this.comparison = normalizeComparison(data.comparison);
this.durationAmount = Math.max(0, data.durationAmount); this.durationAmount = normalizeDurationAmount(data.durationAmount);
this.durationUnit = normalizeDurationUnit(data.durationUnit); this.durationUnit = normalizeDurationUnit(data.durationUnit);
this.userSource = normalizeUserSource(data.userSource); this.userSource = normalizeUserSource(data.userSource);
this.furniSource = normalizeFurniSource(data.furniSource); this.furniSource = normalizeFurniSource(data.furniSource);
@@ -345,8 +350,8 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
return Math.max(0L, System.currentTimeMillis() - timestampMs); return Math.max(0L, System.currentTimeMillis() - timestampMs);
} }
private static long durationToMillis(int amount, int unit) { static long durationToMillis(int amount, int unit) {
long normalizedAmount = Math.max(0L, amount); long normalizedAmount = normalizeDurationAmount(amount);
return switch (unit) { return switch (unit) {
case DURATION_UNIT_MILLISECONDS -> normalizedAmount; case DURATION_UNIT_MILLISECONDS -> normalizedAmount;
@@ -367,22 +372,26 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
return left * right; return left * right;
} }
private static int normalizeTargetTypeExtended(int value) { static int normalizeDurationAmount(int value) {
return Math.max(0, Math.min(MAX_DURATION_AMOUNT, value));
}
static int normalizeTargetTypeExtended(int value) {
return switch (value) { return switch (value) {
case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value;
default -> TARGET_USER; default -> TARGET_USER;
}; };
} }
private static int normalizeCompareValue(int value) { static int normalizeCompareValue(int value) {
return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED; return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED;
} }
private static int normalizeComparison(int value) { static int normalizeComparison(int value) {
return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN; return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN;
} }
private static int normalizeDurationUnit(int value) { static int normalizeDurationUnit(int value) {
return switch (value) { return switch (value) {
case DURATION_UNIT_MILLISECONDS, DURATION_UNIT_SECONDS, DURATION_UNIT_MINUTES, DURATION_UNIT_HOURS, case DURATION_UNIT_MILLISECONDS, DURATION_UNIT_SECONDS, DURATION_UNIT_MINUTES, DURATION_UNIT_HOURS,
DURATION_UNIT_DAYS, DURATION_UNIT_WEEKS, DURATION_UNIT_MONTHS, DURATION_UNIT_YEARS -> value; DURATION_UNIT_DAYS, DURATION_UNIT_WEEKS, DURATION_UNIT_MONTHS, DURATION_UNIT_YEARS -> value;
@@ -51,6 +51,7 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable
private static final int COMPARISON_NOT_EQUAL = 5; private static final int COMPARISON_NOT_EQUAL = 5;
private static final String DELIM = "\t"; private static final String DELIM = "\t";
private static final String FURNI_DELIM = ";"; private static final String FURNI_DELIM = ";";
static final int MAX_ABS_REFERENCE_CONSTANT = 1_000_000_000;
protected int comparison = COMPARISON_EQUAL; protected int comparison = COMPARISON_EQUAL;
protected int referenceMode = REFERENCE_CONSTANT; protected int referenceMode = REFERENCE_CONSTANT;
@@ -123,7 +124,7 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable
int nextTargetType = normalizeTargetTypeExtended(param(params, 0, TARGET_USER)); int nextTargetType = normalizeTargetTypeExtended(param(params, 0, TARGET_USER));
int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL)); int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL));
int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT)); int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT));
int nextReferenceConstantValue = param(params, 3, 0); int nextReferenceConstantValue = normalizeReferenceConstantValue(param(params, 3, 0));
int nextReferenceTargetType = normalizeTargetTypeExtended(param(params, 4, TARGET_USER)); int nextReferenceTargetType = normalizeTargetTypeExtended(param(params, 4, TARGET_USER));
int nextUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); int nextUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER));
int nextFurniSource = normalizeFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); int nextFurniSource = normalizeFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER));
@@ -168,6 +169,10 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable
@Override @Override
public boolean evaluate(WiredContext ctx) { public boolean evaluate(WiredContext ctx) {
if (ctx == null) {
return false;
}
Room room = ctx.room(); Room room = ctx.room();
if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { if (room == null || this.variableToken == null || this.variableToken.isEmpty()) {
@@ -220,14 +225,21 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable
String wiredData = set.getString("wired_data"); String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return;
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data;
try {
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
} catch (RuntimeException exception) {
this.onPickUp();
return;
}
if (data == null) return; if (data == null) return;
this.targetType = normalizeTargetTypeExtended(data.targetType); this.targetType = normalizeTargetTypeExtended(data.targetType);
this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : "")));
this.comparison = normalizeComparison(data.comparison); this.comparison = normalizeComparison(data.comparison);
this.referenceMode = normalizeReferenceMode(data.referenceMode); this.referenceMode = normalizeReferenceMode(data.referenceMode);
this.referenceConstantValue = data.referenceConstantValue; this.referenceConstantValue = normalizeReferenceConstantValue(data.referenceConstantValue);
this.referenceTargetType = normalizeTargetTypeExtended(data.referenceTargetType); this.referenceTargetType = normalizeTargetTypeExtended(data.referenceTargetType);
this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : "")));
this.userSource = normalizeUserSource(data.userSource); this.userSource = normalizeUserSource(data.userSource);
@@ -737,32 +749,36 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable
return (params.length > index) ? params[index] : fallback; return (params.length > index) ? params[index] : fallback;
} }
private static int normalizeTargetTypeExtended(int value) { static int normalizeTargetTypeExtended(int value) {
return switch (value) { return switch (value) {
case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value;
default -> TARGET_USER; default -> TARGET_USER;
}; };
} }
private static int normalizeReferenceMode(int value) { static int normalizeReferenceMode(int value) {
return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT; return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT;
} }
private static int normalizeReferenceFurniSource(int value) { static int normalizeReferenceFurniSource(int value) {
return switch (value) { return switch (value) {
case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value;
default -> WiredSourceUtil.SOURCE_TRIGGER; default -> WiredSourceUtil.SOURCE_TRIGGER;
}; };
} }
private static int normalizeComparison(int value) { static int normalizeComparison(int value) {
return switch (value) { return switch (value) {
case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value; case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value;
default -> COMPARISON_EQUAL; default -> COMPARISON_EQUAL;
}; };
} }
private static int parseInteger(String value) { static int normalizeReferenceConstantValue(int value) {
return Math.max(-MAX_ABS_REFERENCE_CONSTANT, Math.min(MAX_ABS_REFERENCE_CONSTANT, value));
}
static int parseInteger(String value) {
try { try {
return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim());
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
@@ -0,0 +1,21 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
public final class WiredDateRangeInputGuard {
private WiredDateRangeInputGuard() {
}
public static int[] normalizeRange(int startDate, int endDate) {
int start = normalizeTimestamp(startDate);
int end = normalizeTimestamp(endDate);
if (start > end) {
return new int[]{0, 0};
}
return new int[]{start, end};
}
public static int normalizeTimestamp(int value) {
return Math.max(0, value);
}
}
@@ -0,0 +1,76 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class WiredFurniConditionInputGuard {
private WiredFurniConditionInputGuard() {
}
public static int normalizeFurniSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
public static int normalizeUserSource(int value) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
public static int selectedOrNormalizedFurniSource(int value, boolean hasSelectedItems) {
int source = normalizeFurniSource(value);
return (hasSelectedItems && source == WiredSourceUtil.SOURCE_TRIGGER)
? WiredSourceUtil.SOURCE_SELECTED
: source;
}
public static List<Integer> sanitizeItemIds(Collection<Integer> itemIds, int maxCount) {
List<Integer> result = new ArrayList<>();
if (itemIds == null || maxCount < 1) {
return result;
}
for (Integer itemId : itemIds) {
if (itemId == null || itemId < 1 || result.size() >= maxCount) {
continue;
}
result.add(itemId);
}
return result;
}
public static List<Integer> parseLegacyItemIds(String value, int maxCount) {
List<Integer> result = new ArrayList<>();
if (value == null || value.isBlank() || maxCount < 1) {
return result;
}
for (String part : value.split("[;,:\\t]")) {
if (result.size() >= maxCount) {
break;
}
try {
int id = Integer.parseInt(part.trim());
if (id > 0) {
result.add(id);
}
} catch (NumberFormatException ignored) {
// Ignore malformed legacy item ids.
}
}
return result;
}
}
@@ -0,0 +1,92 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.WiredMatchFurniSetting;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class WiredMatchPositionInputGuard {
public static final int MAX_STATE_LENGTH = 512;
private WiredMatchPositionInputGuard() {
}
public static int normalizeFurniSource(int value, boolean hasSelectedSettings) {
int source = switch (value) {
case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR,
WiredSourceUtil.SOURCE_SIGNAL, WiredSourceUtil.SOURCE_TRIGGER -> value;
default -> WiredSourceUtil.SOURCE_TRIGGER;
};
return (hasSelectedSettings && source == WiredSourceUtil.SOURCE_TRIGGER)
? WiredSourceUtil.SOURCE_SELECTED
: source;
}
public static List<WiredMatchFurniSetting> sanitizeSettings(Collection<WiredMatchFurniSetting> settings, Room room) {
List<WiredMatchFurniSetting> result = new ArrayList<>();
if (settings == null || room == null) {
return result;
}
for (WiredMatchFurniSetting setting : settings) {
WiredMatchFurniSetting normalized = sanitizeSetting(setting, room);
if (normalized != null) {
result.add(normalized);
}
if (result.size() >= WiredManager.MAXIMUM_FURNI_SELECTION) {
break;
}
}
return result;
}
public static WiredMatchFurniSetting sanitizeSetting(WiredMatchFurniSetting setting, Room room) {
if (setting == null || room == null) {
return null;
}
return sanitizeParts(setting.item_id, setting.state, setting.rotation, setting.x, setting.y, setting.z, room);
}
public static WiredMatchFurniSetting sanitizeParts(int itemId, String state, int rotation, int x, int y, double z, Room room) {
if (itemId < 1 || room == null) {
return null;
}
HabboItem item = room.getHabboItem(itemId);
if (item == null || rotation < 0 || rotation > 7 || !Double.isFinite(z)) {
return null;
}
if (x < Short.MIN_VALUE || x > Short.MAX_VALUE || y < Short.MIN_VALUE || y > Short.MAX_VALUE) {
return null;
}
if (room.getLayout() != null && room.getLayout().getTile((short) x, (short) y) == null) {
return null;
}
return new WiredMatchFurniSetting(itemId, normalizeState(state), rotation, x, y, z);
}
public static String normalizeState(String state) {
if (state == null) {
return "";
}
String normalized = state.replace('\t', ' ').replace('\r', ' ').replace('\n', ' ');
if (normalized.length() > MAX_STATE_LENGTH) {
return normalized.substring(0, MAX_STATE_LENGTH);
}
return normalized;
}
}
@@ -0,0 +1,14 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
public final class WiredUserActionInputGuard {
private WiredUserActionInputGuard() {
}
public static boolean isRecentTimestamp(long timestamp, long now, long windowMs) {
if (timestamp < 1 || timestamp > now || windowMs < 1) {
return false;
}
return (now - timestamp) <= windowMs;
}
}

Some files were not shown because too many files have changed in this diff Show More