') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix: make donation limit handling consistent across all paths by tastybento · Pull Request #449 · BentoBoxWorld/Level · GitHub
Skip to content

fix: make donation limit handling consistent across all paths - #449

Merged
tastybento merged 1 commit into
developfrom
fix/donation-limit-consistency
Jul 17, 2026
Merged

fix: make donation limit handling consistent across all paths#449
tastybento merged 1 commit into
developfrom
fix/donation-limit-consistency

Conversation

@tastybento

Copy link
Copy Markdown
Member

Follow-ups from the 2.28.0 release review of the donation limit feature. Addresses review issues 1–5.

Changes

  • Case-insensitive limit lookups (review Lowest level config option #1): blockLimits keys are now stored lowercased and getLimit(String) lowercases its lookup, so the level calculation (which lowercases donation keys) and the donation command/panel paths (which pass custom IDs as-is) always resolve to the same limit — including for mixed-case custom block IDs (e.g. Oraxen item IDs).
  • Single source of truth for donated points (review Exponential Island Level calculation #4): new Utils.calculateDonatedPoints() (current values, world overrides, limit capping) used by both tidyUp() and the donation panel's "Currently donated" info pane, so the GUI figure now matches what the level actually uses instead of the stored donation-time total.
  • No more heuristic blockObj reconstruction (review Island level chat prefix color #2): DonationPanel.computeDonationTotals carries the resolved blockObj from the slot walk instead of re-deriving it via donationId.contains(":") ? … : Material.matchMaterial(…), which could mis-resolve colon-less custom IDs.
  • Report lines sum to the total (review Count chest content in Island Level #3): getReport() caps donated lines at the current block limit and marks capped entries, matching the scanned-blocks section.
  • 🔡 Clear message when everything is at its limit (review Configurable top ten #5): new island.donate.limit-reached-all key (translated in all 17 locales). The panel confirm button and /island donate inv now send it instead of a misleading "empty" message or a 0-point confirmation prompt.

Test note

Mockito returns 0 (not null) for unstubbed Integer-returning methods — an unstubbed getLimit() mock reads as "limit of 0". The pre-existing testExecuteInvShowsConfirmationPrompt was silently exercising the everything-at-limit path with a 0-point prompt because of this. Test setups now stub getLimit(any()) to return null (matching the real BlockConfig when no limit is configured), as IslandLevelCalculatorTidyUpTest already did.

New tests: BlockConfigLimitsTest (case-insensitivity contract), UtilsCalculateDonatedPointsTest (value/limit/cap math), plus an all-at-limit /island donate inv command test.

Full suite: 237 tests, 0 failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y6f59pxCkwS8QXtJSe1nq6

Follow-ups from the 2.28.0 release review of the donation limit feature:
- BlockConfig: store blockLimits keys lowercased and lowercase String
lookups in getLimit(), so the level calculation (which lowercases
donation keys) and the donation command/panel paths (which pass custom
IDs as-is) always resolve to the same limit, including for mixed-case
custom block IDs.
- Extract Utils.calculateDonatedPoints() as the single source of truth
for donated points (current values, world overrides, limit capping);
used by both tidyUp() and the donation panel info pane, so the
"Currently donated" figure now matches what the level actually uses
instead of the stored donation-time total.
- DonationPanel: carry the resolved blockObj from the slot walk instead
of re-deriving it from the donation ID via Material.matchMaterial(),
which could mis-resolve colon-less custom IDs.
- getReport(): cap donated lines at the current block limit so they sum
to the reported donated total, and mark capped entries.
- New island.donate.limit-reached-all locale key (all 17 locales): shown
when everything offered is already at its limit, instead of a
misleading "empty" message (panel) or a 0-point confirm prompt (inv).
- Tests: cover the new paths; stub getLimit to return null in setups
because Mockito returns 0 for unstubbed Integer methods, which reads
as "limit of 0" and silently exercised the wrong path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6f59pxCkwS8QXtJSe1nq6
@sonarqubecloud

Copy link
Copy Markdown

@tastybento
tastybento merged commit caf53d2 into developJul 17, 2026
3 checks passed
@tastybento
tastybento deleted the fix/donation-limit-consistency branch July 17, 2026 04:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tastybento