Skip to content

Update CountableObjective to support complex targets#448

Open
ZoeWithTheE wants to merge 9 commits intogabber235:developfrom
ZoeWithTheE:patch-1
Open

Update CountableObjective to support complex targets#448
ZoeWithTheE wants to merge 9 commits intogabber235:developfrom
ZoeWithTheE:patch-1

Conversation

@ZoeWithTheE
Copy link
Copy Markdown

@ZoeWithTheE ZoeWithTheE commented Apr 4, 2026

Feat: replace int target with range/set string parsing

Target now accepts a string like "28-61,63,70.." instead of a single int. Supports inclusive ranges, individual values, open-ended upper (32..) and lower (..10) bounds, and combinations. Display tag strips open-ended syntax for clean rendering (e.g. "32.." shows as "32").

Summary by CodeRabbit

  • New Features

    • Quest objectives now accept flexible target specs: exact values, inclusive ranges, open-ended bounds, wildcards, and comma-separated combinations.
    • Completion checks compare the current count against the parsed target spec.
    • Objective displays show a normalized target representation and cache the current count for placeholders.
    • Specs are simplified by removing subsumed entries and collapsing to universal when applicable.
  • Tests

    • Added comprehensive parsing and semantics tests for target specs.
  • Chores

    • Test setup updated to run on the JUnit 5 platform with Kotest.

Feat: replace int target with range/set string parsing

Target now accepts a string like "28-61,63,70.." instead of a single int.
Supports inclusive ranges, individual values, open-ended upper (32..) and
lower (..10) bounds, and combinations. Display tag strips open-ended syntax
for clean rendering (e.g. "32.." shows as "32").
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

CountableObjective's target changed from an integer Var to a string-based TargetSpec parsed from token syntax. Completion now checks membership with TargetSpec.contains(currentCount). Placeholder rendering uses TargetSpec.display() and caches <count>; player == null returns early.

Changes

Cohort / File(s) Summary
CountableObjective
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt
target type changed from Var<Int>Var<String> (ConstVar("0")). Completion logic updated to parse target into TargetSpec and use targetSpec.contains(currentCount). Placeholder rendering: <count> cached, <target> uses targetSpec.display(). Early return when player == null.
New TargetSpec model
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpec.kt
Added sealed TargetSpec and concrete types: ExactTarget, RangeTarget, LowerBoundTarget, UpperBoundTarget, UniversalTarget, EmptyTarget, CompositeTarget. Implements contains, subsumes, parse(raw), token parsing, deduplication, subsumption-dropping, and collapse-to-universal logic.
Tests for TargetSpec
extensions/QuestExtension/src/test/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpecTest.kt
Added Kotest suite validating parsing, simplification rules, contains semantics for every spec type, and subsumes relationships across specs, including many edge cases and invalid-token behavior.
Build config (tests)
extensions/QuestExtension/build.gradle.kts
Added Kotest dependencies (kotest-runner-junit5, kotest-assertions-core) with kotestVersion = "6.1.10" and configured tasks.test { useJUnitPlatform() } to run tests on JUnit Platform.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble through specs with twitchy feet,
Commas, ranges, stars — a hopping feat,
Counts snug in burrows, targets in sight,
I parse and simplify by moonlight—🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: updating CountableObjective to support complex targets via string-based parsing instead of integer-only values.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt (2)

95-111: Use one snapshot of count and target per render.

Lines 96-111 read both vars twice: once through isComplete(player) and again for <count> / <target>. For backed or otherwise dynamic vars, that can produce a completed state from one value and render a different one. CachableFactObjective.display() already avoids this by resolving its values once before branching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`
around lines 95 - 111, Read and cache the dynamic values once at the start of
display(player): call count.get(player) and target.get(player) into local vals
(and resolve display.get(player) too if needed), then use those snapshots for
the completion check and all subsequent replacements instead of calling
isComplete(player) (which currently reads count/target again); replace the
isComplete(player) call with a local check using
matchesTarget(cachedCount.absoluteValue, cachedTarget) and use cached values in
replaceTagPlaceholders and displayTarget so the rendered text and completion
state are consistent (refer to functions/fields count.get, target.get,
isComplete, matchesTarget, display.get, criteria.matches, displayTarget,
inactiveObjectiveDisplay, countableObjectiveDisplay, showingObjectiveDisplay).

87-87: Wrap the @Help text to the repo's 120-column limit.

Line 87 is well past the line-length cap; please split the string across lines.

As per coding guidelines, "Use 4 spaces for indentation and wrap lines at 120 characters in Kotlin code and Gradle Kotlin scripts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`
at line 87, The `@Help` string on CountableObjective (the annotation at line
containing `@Help` in CountableObjective.kt) exceeds the 120-column limit; split
the long literal into multiple shorter string segments concatenated inside the
`@Help`(...) call (e.g. "first chunk " + "second chunk...") or use an equivalent
constant string approach so each source line is <=120 chars, preserving the
exact help text and punctuation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 52-56: The range parsing in CountableObjective (the
trimmed.contains("-") branch) currently splits on every '-' and accepts more
than two pieces, so tokens like "1-2-3" become "1..2"; change the logic in
CountableObjective.kt to reject malformed ranges by ensuring rangeParts.size ==
2 before using rangeParts[0] and rangeParts[1], then parse both with toIntOrNull
and only return true if both are non-null and count in from..to; any other case
should be treated as invalid (return false or propagate the invalid token
behavior used elsewhere).
- Around line 67-71: The displayTarget helper currently trims open-ended syntax
only once on the whole string, which lets tokens like "32..,50" or "..10,28.."
leak; update displayTarget in CountableObjective (displayTarget) to split the
raw string by commas, normalize each token individually by trimming and removing
leading ".." or trailing ".." (same logic as currently applied to the whole
string), and then rejoin the normalized tokens with commas so each target
segment is rendered without open-ended syntax.

---

Nitpick comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 95-111: Read and cache the dynamic values once at the start of
display(player): call count.get(player) and target.get(player) into local vals
(and resolve display.get(player) too if needed), then use those snapshots for
the completion check and all subsequent replacements instead of calling
isComplete(player) (which currently reads count/target again); replace the
isComplete(player) call with a local check using
matchesTarget(cachedCount.absoluteValue, cachedTarget) and use cached values in
replaceTagPlaceholders and displayTarget so the rendered text and completion
state are consistent (refer to functions/fields count.get, target.get,
isComplete, matchesTarget, display.get, criteria.matches, displayTarget,
inactiveObjectiveDisplay, countableObjectiveDisplay, showingObjectiveDisplay).
- Line 87: The `@Help` string on CountableObjective (the annotation at line
containing `@Help` in CountableObjective.kt) exceeds the 120-column limit; split
the long literal into multiple shorter string segments concatenated inside the
`@Help`(...) call (e.g. "first chunk " + "second chunk...") or use an equivalent
constant string approach so each source line is <=120 chars, preserving the
exact help text and punctuation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d30325a1-ad9c-488f-9397-844952ad5c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 1aec3a6 and 293e14a.

📒 Files selected for processing (1)
  • extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt

ZoeWithTheE and others added 2 commits April 4, 2026 13:36
…st/entries/audience/objectives/CountableObjective.kt

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
fix: address CodeRabbit review comments

- Add limit=2 to range split to reject malformed tokens like "1-2-3"
- Normalize displayTarget per comma-separated token so combinations
  like "32..,50" render correctly instead of leaking open-ended syntax
- Snapshot count/target/display vars once per display() call to ensure
  completion state and rendered values are always consistent
- Wrap @Help string to 120-column limit
- Fix stray brace syntax error in matchesTarget
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt (1)

68-72: ⚠️ Potential issue | 🟡 Minor

Still normalize each comma-separated target before rendering.

displayTarget still strips .. only once on the full string, so values like 32..,50 or ..10,28.. will leak open-ended syntax into <target>.

💡 Proposed fix
-private fun displayTarget(raw: String): String = when {
-    raw.endsWith("..") -> raw.dropLast(2).trim()
-    raw.startsWith("..") -> raw.drop(2).trim()
-    else -> raw
-}
+private fun displayTarget(raw: String): String = raw
+    .split(",")
+    .joinToString(",") { part ->
+        val trimmed = part.trim()
+        when {
+            trimmed.endsWith("..") -> trimmed.dropLast(2).trim()
+            trimmed.startsWith("..") -> trimmed.drop(2).trim()
+            else -> trimmed
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`
around lines 68 - 72, displayTarget currently strips leading/trailing ".." only
once on the whole raw string, allowing open-ended markers to leak when raw is a
comma-separated list; update displayTarget to split raw on commas, normalize
each segment by trimming and removing any leading ".." or trailing ".." (e.g.,
dropFirst(2)/dropLast(2) or equivalent) on each piece, then rejoin the
normalized segments with commas (preserving spacing) so values like "32..,50" or
"..10,28.." render without open-ended syntax.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 52-59: In CountableObjective.kt remove the stray closing brace
that prematurely ends the when expression before the else branch: locate the
when handling the "trimmed" variable (the branch handling trimmed.contains("-")
which computes from/to and checks count in from..to) and delete the extra `}` so
the subsequent `else -> trimmed.toIntOrNull() == count` remains inside the same
when block, restoring valid Kotlin syntax for the function/method that evaluates
the objective.

---

Duplicate comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 68-72: displayTarget currently strips leading/trailing ".." only
once on the whole raw string, allowing open-ended markers to leak when raw is a
comma-separated list; update displayTarget to split raw on commas, normalize
each segment by trimming and removing any leading ".." or trailing ".." (e.g.,
dropFirst(2)/dropLast(2) or equivalent) on each piece, then rejoin the
normalized segments with commas (preserving spacing) so values like "32..,50" or
"..10,28.." render without open-ended syntax.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 69c8ca20-aa30-45b9-986f-1c6a6c4ee27d

📥 Commits

Reviewing files that changed from the base of the PR and between 293e14a and 479f7ea.

📒 Files selected for processing (1)
  • extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt

@Marten-Mrfc
Copy link
Copy Markdown
Collaborator

Marten-Mrfc commented Apr 5, 2026

@gabber235 I do think the matchesTarget function is not 100% related to the CountableObjective and rather should be a function on its own. Idk what you think about it but for the rest I find it okay.

Edit:
As an addition Var<String> would probably return a lot of conflicts... So I think the extra file would be better.

Copy link
Copy Markdown
Owner

@gabber235 gabber235 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea for this, but I really don't like the execution. First off, there are a bunch of edge cases that are not covered and don't really throw an error or any warning to the user.

Secondly, I think the sanitization should be done much better. Currently, your display target strips out the .., but that makes it really confusing.

I propose you make this a bit more proper with sealed interfaces and subclasses. Use composition to both parse, check if a number is a range, and simplify (like for the , thing: if we have one range which is captured fully within another, we should remove it). So for example, 23..,10..,50 would only become 10.. because all the other ones are captured within it.

You also need a way to represent no valid values, or where all possible values are valid. For example, ..15,10.. allows all values. An empty string `` allows nothing. This way, if we have ,,, we can easily clean it out.

I would make a sealed interface with a companion object containing a parse function which checks how to parse the current thing, then delegates to specific parse functions of the subclasses. Each subclass would have parse, display, contains, and probably a few more functions for the simplification process.

Then we can use it in the display function nicely.

@gabber235 gabber235 force-pushed the develop branch 3 times, most recently from 4d9cde8 to 1e92538 Compare April 5, 2026 14:11
@ZoeWithTheE
Copy link
Copy Markdown
Author

Thanks for the feedback, rewrote it with a sealed interface. TargetSpec has six impls: ExactTarget, RangeTarget, LowerBoundTarget (N..), UpperBoundTarget (..N), UniversalTarget, and EmptyTarget. Each has contains, subsumes, and display. The parse companion does the comma splitting, delegates to subclass parsers, drops invalid tokens, then simplifies by removing anything sub'd by another spec, so 23..,10..,50 collapses to 10.. as you described. Blank/fully invalid -> EmptyTarget, overlapping ..N and N.. -> UniversalTarget.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt (1)

193-196: Unnecessary null coalescing on non-nullable return types.

After the null guard clause at line 191, player is smartcast to non-null Player. At lines 194-195, target.get(player) and display.get(player) invoke the Var<T> interface method which returns non-nullable T, so the ?: "" operators are unnecessary.

Proposed simplification
         val currentCount = count.get(player)
-        val targetSpec = TargetSpec.parse(target.get(player) ?: "")
-        val displayStr = display.get(player) ?: ""
+        val targetSpec = TargetSpec.parse(target.get(player))
+        val displayStr = display.get(player)
         val complete = targetSpec.contains(currentCount.absoluteValue)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`
around lines 193 - 196, The code performs unnecessary null-coalescing on values
that are already non-null: after the null-guard that smart-casts player to
Player, remove the redundant "?: \"\"" on calls to target.get(player) and
display.get(player) in CountableObjective (the lines creating targetSpec and
displayStr). Keep using TargetSpec.parse(target.get(player)) and
display.get(player) directly and leave currentCount and complete logic
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 82-97: The parseToken function treats strings containing "-" as
ranges before attempting exact integer parsing, causing "-5" to be split into a
malformed range and return null; update parseToken so it attempts
token.toIntOrNull() (creating ExactTarget) before the token.contains("-") branch
(or add a guard that treats a leading "-" followed by digits as an exact
integer) so negative integers are parsed into ExactTarget, ensuring
RangeTarget(from,to) still handles proper "a-b" ranges; refer to parseToken,
ExactTarget, RangeTarget, LowerBoundTarget, UpperBoundTarget, and
UniversalTarget when applying the change.

---

Nitpick comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 193-196: The code performs unnecessary null-coalescing on values
that are already non-null: after the null-guard that smart-casts player to
Player, remove the redundant "?: \"\"" on calls to target.get(player) and
display.get(player) in CountableObjective (the lines creating targetSpec and
displayStr). Keep using TargetSpec.parse(target.get(player)) and
display.get(player) directly and leave currentCount and complete logic
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3cf0d2cb-9571-4159-b197-f5854def181a

📥 Commits

Reviewing files that changed from the base of the PR and between 479f7ea and c123661.

📒 Files selected for processing (1)
  • extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt

Refactor target parsing logic to return null for invalid tokens.
@ZoeWithTheE ZoeWithTheE requested a review from gabber235 April 5, 2026 16:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt (1)

121-141: ⚠️ Potential issue | 🟡 Minor

<target> still leaks open-ended syntax.

LowerBoundTarget.display() and UpperBoundTarget.display() return 32.. / ..10, so Line 208 renders the parser syntax back to players. That reintroduces the same display issue for open-ended targets and composites that include them.

💡 Proposed fix
 data class LowerBoundTarget(val min: Int) : TargetSpec {
@@
-    override fun display() = "$min.."
+    override fun display() = min.toString()
 }
@@
 data class UpperBoundTarget(val max: Int) : TargetSpec {
@@
-    override fun display() = "..$max"
+    override fun display() = max.toString()
 }

Also applies to: 205-208

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`
around lines 121 - 141, LowerBoundTarget.display() and
UpperBoundTarget.display() currently return parser-style open-ended strings like
"32.." and "..10", leaking internal syntax to players; change their display
implementations to human-friendly phrases (e.g., ">= 32" for
LowerBoundTarget.display and "<= 10" for UpperBoundTarget.display) (or use the
project’s localization helper if available) so any composite displays that
include LowerBoundTarget or UpperBoundTarget no longer show parser syntax;
update the display() methods in the LowerBoundTarget and UpperBoundTarget
classes accordingly.
🧹 Nitpick comments (1)
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt (1)

52-79: Extract the parse phases into helpers instead of documenting them inline.

TargetSpec.parse() is doing tokenization, simplification, and universal-coverage detection in one block, and Lines 62-69 rely on inline comments to explain the flow. Pulling those phases into small private helpers would keep the function focused and remove the guideline violation.

As per coding guidelines "Keep functions short and focused; prefer guard clauses over nested conditionals" and "Avoid inline comments; refactor unclear code instead of adding explanatory comments".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`
around lines 52 - 79, TargetSpec.parse currently performs tokenization (using
parseToken), simplification and universal-coverage detection inline; extract
each phase into small private helpers to keep parse short: create e.g. private
fun tokenizeAndParse(raw: String): List<TargetSpec> that does the
split/map/filter/mapNotNull with parseToken and guard for EmptyTarget, private
fun simplifyTokens(tokens: List<TargetSpec>): List<TargetSpec> that implements
the subsumption filter (the simplified logic currently using candidate/other),
and private fun detectsUniversal(tokens: List<TargetSpec>): Boolean that
computes hasLower/hasUpper using UpperBoundTarget and LowerBoundTarget and
returns true when the computed lower/upper imply UniversalTarget; then have
parse delegate to these helpers and return EmptyTarget, UniversalTarget, a
single token, or CompositeTarget as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 82-90: The parser in CountableObjective.parseToken allows signed
targets (e.g., "-5", "-5..", "..-5") but the completion check uses
currentCount.absoluteValue, causing mismatches; update the matching logic (the
code that checks targets around where currentCount.absoluteValue is used and the
TargetSpec implementations like ExactTarget, LowerBoundTarget, UpperBoundTarget)
to compare using the signed currentCount (or otherwise normalize both parsed
targets and currentCount the same way) so negative targets are matched correctly
and lower/upper bounds respect the sign produced by parseToken.
- Around line 62-65: The subsumption pass removes duplicate-value tokens because
it compares references (other !== candidate) so two equal tokens are treated as
distinct and subsume each other; fix by deduplicating the tokens list before
running the subsumption logic (e.g., replace the source list with
tokens.distinct() or tokens.distinctBy { /* key */ } and then run the existing
tokens.filter { candidate -> tokens.none { other -> other !== candidate &&
other.subsumes(candidate) } } logic against that deduplicated list); reference
the tokens variable, the filter lambda (candidate/other) and the subsumes(...)
method when making the change.

---

Duplicate comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 121-141: LowerBoundTarget.display() and UpperBoundTarget.display()
currently return parser-style open-ended strings like "32.." and "..10", leaking
internal syntax to players; change their display implementations to
human-friendly phrases (e.g., ">= 32" for LowerBoundTarget.display and "<= 10"
for UpperBoundTarget.display) (or use the project’s localization helper if
available) so any composite displays that include LowerBoundTarget or
UpperBoundTarget no longer show parser syntax; update the display() methods in
the LowerBoundTarget and UpperBoundTarget classes accordingly.

---

Nitpick comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt`:
- Around line 52-79: TargetSpec.parse currently performs tokenization (using
parseToken), simplification and universal-coverage detection inline; extract
each phase into small private helpers to keep parse short: create e.g. private
fun tokenizeAndParse(raw: String): List<TargetSpec> that does the
split/map/filter/mapNotNull with parseToken and guard for EmptyTarget, private
fun simplifyTokens(tokens: List<TargetSpec>): List<TargetSpec> that implements
the subsumption filter (the simplified logic currently using candidate/other),
and private fun detectsUniversal(tokens: List<TargetSpec>): Boolean that
computes hasLower/hasUpper using UpperBoundTarget and LowerBoundTarget and
returns true when the computed lower/upper imply UniversalTarget; then have
parse delegate to these helpers and return EmptyTarget, UniversalTarget, a
single token, or CompositeTarget as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cff43815-2b6c-4bbc-be70-114d305f77ff

📥 Commits

Reviewing files that changed from the base of the PR and between c123661 and f5e2ef0.

📒 Files selected for processing (1)
  • extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt

delegate parsing to subclasses, snippet display, drop absoluteValue
@ZoeWithTheE
Copy link
Copy Markdown
Author

Parsing is now fully delegated and each subclass has its own tryParse function, and the central parse iterates a list of parsers. Removed absoluteValue as suggested. Display strings are all snippet-backed with tag placeholders (, , ) so they're translatable, including the composite separator since that varies by language.

Copy link
Copy Markdown
Owner

@gabber235 gabber235 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is really taking shape now! There are a few small things that should still be fixed. Additionally, I would recommend writing some tests for the parsing, including golden tests (checking expected outputs), some edge cases, and invalid inputs (where invalid strings are given) to validate correct behaviour.

Comment on lines +144 to +150
fun tryParse(token: String): RangeTarget? {
if (!token.contains("-")) return null
val parts = token.split("-", limit = 2)
val from = parts[0].trim().toIntOrNull()
val to = parts.getOrNull(1)?.trim()?.toIntOrNull()
return if (from != null && to != null && from <= to) RangeTarget(from, to) else null
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait is this still correct? Your comment says [min]..[max] but I believe this is checking for [min]-[max].

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again tests would likely really help with catching stuff like this 😉

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh dreaded tests 😅
Where do you want them? In file or another tests location, had a quick poke around and didn't see anything obvious. I'll be very busy over the next few days, so fixes may be slow

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put in the extension testing folder? Make the package the same as the where this is located.

Comment on lines +198 to +200
fun tryParse(token: String): UniversalTarget? =
if (token == "*") UniversalTarget else null
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also make .. parse as universal? As it follows a bit more nicely from the other targets

@ZoeWithTheE
Copy link
Copy Markdown
Author

Split TargetSpec out into its own file, the snippet delegates in CountableObjective init through Koin at class load so anything in the same file isn't testable without a full server context. Display is a private extension function in CountableObjective using a when over all the subclasses.

Fixed the KDoc on RangeTarget (said [min]..[max], it's [min]-[max]), ".." now parses as UniversalTarget alongside "*", and added distinct() before the subsumption pass so parse("5,5") gives ExactTarget(5) instead of EmptyTarget.

Tests are in extensions/QuestExtension/src/test with the same package as requested, covering golden cases, simplification, edge cases, contains and subsumes.

- Extract TargetSpec sealed interface and subclasses into TargetSpec.kt so
  the parser is testable without a Koin/server context
- Display moved to private extension function in CountableObjective.kt
- Fix RangeTarget KDoc ([min]..[max] -> [min]-[max])
- ".." now parses as UniversalTarget alongside "*"
- Add distinct() before subsumption pass to fix parse("5,5") -> EmptyTarget
- Add tests in src/test with same package covering parse, contains, subsumes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
extensions/QuestExtension/src/test/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpecTest.kt (1)

106-135: Consider adding tests for negative integers.

The past review discussion addressed negative integer parsing (e.g., "-5" as exact, "-5.." as lower bound, "..-5" as upper bound). Adding explicit tests would document and protect this behavior:

test("negative exact value") {
    TargetSpec.parse("-5") shouldBe ExactTarget(-5)
}

test("negative lower bound") {
    TargetSpec.parse("-5..") shouldBe LowerBoundTarget(-5)
}

test("negative upper bound") {
    TargetSpec.parse("..-5") shouldBe UpperBoundTarget(-5)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/test/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpecTest.kt`
around lines 106 - 135, Add tests to cover negative integer parsing by extending
the "parse invalid and edge inputs" context: add a test asserting
TargetSpec.parse("-5") shouldBe ExactTarget(-5), another asserting
TargetSpec.parse("-5..") shouldBe LowerBoundTarget(-5), and a third asserting
TargetSpec.parse("..-5") shouldBe UpperBoundTarget(-5); place them alongside the
existing cases so TargetSpec.parse, ExactTarget, LowerBoundTarget, and
UpperBoundTarget behavior for negative values is explicitly verified.
extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpec.kt (1)

144-148: Clarify CompositeTarget.subsumes semantics.

The current implementation returns true if any spec in the composite subsumes other:

override fun subsumes(other: TargetSpec) = specs.any { it.subsumes(other) }

This is correct for the simplification pass (removing specs subsumed by any member), but semantically a composite subsumes another spec only if the composite's union covers all values of other. For example, CompositeTarget([ExactTarget(1), ExactTarget(3)]) would not subsume RangeTarget(1,3) even though neither individual spec does.

Since this is only used internally for simplification and the current behavior is sufficient for that purpose, this is fine as-is. However, a brief comment clarifying this design choice would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpec.kt`
around lines 144 - 148, Document CompositeTarget.subsumes semantics: add a brief
comment on the CompositeTarget data class (and specifically the subsumes
override) explaining that subsumes returns true if any contained TargetSpec
subsumes the other (i.e., it's a heuristic for the simplification pass) and does
not mean the union of specs fully covers the other TargetSpec; reference
CompositeTarget, subsumes(other: TargetSpec) and TargetSpec in the comment so
future maintainers understand this intentional design choice and its limited
use.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpec.kt`:
- Around line 82-91: RangeTarget.tryParse currently splits on '-' and fails for
negative ranges; update RangeTarget.tryParse to use a regex like (-?\d+)-(-?\d+)
to capture two signed integers, parse each with toIntOrNull, validate from <=
to, and return RangeTarget(from, to) only on success; also update the `@Help` text
in CountableObjective to reflect that negative ranges (e.g., -5--2) are now
supported (or explicitly state limitation if you choose not to implement
parsing).

---

Nitpick comments:
In
`@extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpec.kt`:
- Around line 144-148: Document CompositeTarget.subsumes semantics: add a brief
comment on the CompositeTarget data class (and specifically the subsumes
override) explaining that subsumes returns true if any contained TargetSpec
subsumes the other (i.e., it's a heuristic for the simplification pass) and does
not mean the union of specs fully covers the other TargetSpec; reference
CompositeTarget, subsumes(other: TargetSpec) and TargetSpec in the comment so
future maintainers understand this intentional design choice and its limited
use.

In
`@extensions/QuestExtension/src/test/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpecTest.kt`:
- Around line 106-135: Add tests to cover negative integer parsing by extending
the "parse invalid and edge inputs" context: add a test asserting
TargetSpec.parse("-5") shouldBe ExactTarget(-5), another asserting
TargetSpec.parse("-5..") shouldBe LowerBoundTarget(-5), and a third asserting
TargetSpec.parse("..-5") shouldBe UpperBoundTarget(-5); place them alongside the
existing cases so TargetSpec.parse, ExactTarget, LowerBoundTarget, and
UpperBoundTarget behavior for negative values is explicitly verified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8e497166-e448-49b0-8710-8d643c87b5a0

📥 Commits

Reviewing files that changed from the base of the PR and between f5e2ef0 and ec649bd.

📒 Files selected for processing (4)
  • extensions/QuestExtension/build.gradle.kts
  • extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/CountableObjective.kt
  • extensions/QuestExtension/src/main/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpec.kt
  • extensions/QuestExtension/src/test/kotlin/com/typewritermc/quest/entries/audience/objectives/TargetSpecTest.kt

@ZoeWithTheE
Copy link
Copy Markdown
Author

all the concrete objectives (BreakBlock, BreedMob, Fish, KillEntity, PlaceBlock, ShearEntity, SmeltObjective, Travel) use CacheableFactObjectiveProgressTracking with a Var<Int> target, would it be worth swapping that for TargetSpec? SmeltObjective increments by item.amount so an exact target can get skipped if a batch pushes you past it. could also be cool to bring the same to the criteria system

ZoeWithTheE and others added 2 commits April 10, 2026 21:08
Negative ranges like -5--2 silently fail (split on first dash gives an
empty left-hand side). Not worth regex complexity for quest objectives
where fact values are always non-negative.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Facts can be negative so -10--3 and -5-3 style ranges are valid.
Switched to regex (-?\d+)-(-?\d+) to handle them correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to 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.

3 participants