Skip to content

Commit 842f6a3

Browse files
committed
fix: also migrate TableDiceAnimationCoordinator off the removed World.spawn Consumer overload
1.1.1 only fixed the four call sites inside DisplayEntities. The opening-roll animation in TableDiceAnimationCoordinator has two more (spawnDie + spawnResultLabel) that still bind to the deprecated org.bukkit.util.Consumer overload, so /mahjong botmatch and any other trigger of the dice animation still threw NoSuchMethodError on Paper 1.21+. - Switch both helpers to World.spawn(Location, Class) followed by property assignments, matching the DisplayEntities pattern. - Preserve the return value from spawnResultLabel so the caller can continue to track the spawned TextDisplay. To stop this regressing again, add an architecture boundary test that scans every production Java/Kotlin source and rejects any three-argument World.spawn(Location, Class, lambda) call site. The portable two-argument spawn is fine; new lambda-based spawns will fail CI. Bump to 1.1.2 and document the missed-callsite hotfix in CHANGELOG.
1 parent c9d87ca commit 842f6a3

4 files changed

Lines changed: 83 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 1.1.2 - 2026-06-16
4+
5+
Follow-up hotfix to 1.1.1. The Paper 1.21+ `NoSuchMethodError` regression had two more call sites that were not migrated in 1.1.1.
6+
7+
中文更新日志:
8+
9+
- 漏修补全: 1.1.1 只迁移了 `DisplayEntities` 里的 4 处 `World.spawn(..., Consumer)`,但 `TableDiceAnimationCoordinator` 里另有 2 处(开局骰子动画的骰子实体和结果文本),它们仍然命中已被移除的 `org.bukkit.util.Consumer` 重载。在 Paper 1.21+ 上一执行 `/mahjong botmatch` 等会触发开局动画的命令就抛 `NoSuchMethodError`。本版本把这两处改为同样的 spawn-then-configure 模式。
10+
- 防回归门禁: 新增 `ArchitectureBoundaryTest.World spawn callers do not use the removed Consumer overload`,扫描所有生产 Java/Kotlin 源码,禁止任何 `World.spawn(Location, Class, lambda)` 三参数调用形式。下次有人无意把 lambda 写回 spawn 调用就会在 CI 失败,避免再次引入相同 bug。
11+
12+
English Release Notes:
13+
14+
- Missed callsites: 1.1.1 only migrated the four `World.spawn(..., Consumer)` calls in `DisplayEntities`, but `TableDiceAnimationCoordinator` had two more (the opening-roll dice entities and the result text label) that still bound to the removed `org.bukkit.util.Consumer` overload. Any command that triggers the opening dice animation, e.g. `/mahjong botmatch`, threw `NoSuchMethodError` on Paper 1.21+. Both sites now use the same spawn-then-configure pattern.
15+
- Regression gate: added `ArchitectureBoundaryTest.World spawn callers do not use the removed Consumer overload`. It scans every production Java/Kotlin source file and rejects any three-argument `World.spawn(Location, Class, lambda)` call site, so the same class of bug fails CI before it can ship again.
16+
317
## 1.1.1 - 2026-06-16
418

519
Hotfix release for Paper 1.21+ servers. 1.1.0 was unusable on Paper 1.21.x because every entity render path tripped a `NoSuchMethodError`.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ plugins {
1414
}
1515

1616
group = "top.ellan"
17-
version = "1.1.1"
17+
version = "1.1.2"
1818

1919
val minimumPaperDevBundleVersion = "1.20.1-R0.1-SNAPSHOT"
2020
val latestPaperDevBundleVersion = "26.2-rc-2.build.9-alpha"

src/main/java/top/ellan/mahjong/table/presentation/TableDiceAnimationCoordinator.java

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,22 @@ private TextDisplay updateResultLabel(Component text) {
166166
}
167167

168168
private TextDisplay spawnResultLabel(Component text) {
169-
return this.world.spawn(
169+
// See spawnDie: avoid the org.bukkit.util.Consumer overload that 1.21+ removed.
170+
TextDisplay spawned = this.world.spawn(
170171
this.center.clone().add(0.0D, RESULT_LABEL_Y, 0.0D),
171-
TextDisplay.class,
172-
spawned -> {
173-
spawned.setPersistent(false);
174-
spawned.text(text);
175-
spawned.setSeeThrough(false);
176-
spawned.setShadowed(true);
177-
spawned.setDefaultBackground(false);
178-
spawned.setBillboard(Display.Billboard.CENTER);
179-
spawned.setLineWidth(180);
180-
spawned.setViewRange(32.0F);
181-
spawned.setBrightness(new Display.Brightness(15, 15));
182-
spawned.setBackgroundColor(Color.fromARGB(104, 20, 20, 24));
183-
}
172+
TextDisplay.class
184173
);
174+
spawned.setPersistent(false);
175+
spawned.text(text);
176+
spawned.setSeeThrough(false);
177+
spawned.setShadowed(true);
178+
spawned.setDefaultBackground(false);
179+
spawned.setBillboard(Display.Billboard.CENTER);
180+
spawned.setLineWidth(180);
181+
spawned.setViewRange(32.0F);
182+
spawned.setBrightness(new Display.Brightness(15, 15));
183+
spawned.setBackgroundColor(Color.fromARGB(104, 20, 20, 24));
184+
return spawned;
185185
}
186186

187187
private void animateRollingDie(ItemDisplay display, double baseX, long tick, double driftZ, double tiltBias, float yawBias) {
@@ -215,27 +215,31 @@ private void positionSettledDie(ItemDisplay display, double xOffset, float yaw)
215215
}
216216

217217
private ItemDisplay spawnDie(Location location, int point) {
218-
return this.world.spawn(location, ItemDisplay.class, spawned -> {
219-
spawned.setPersistent(false);
220-
spawned.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.HEAD);
221-
spawned.setInterpolationDuration(1);
222-
spawned.setInterpolationDelay(0);
223-
PaperCompatibility.setTeleportDuration(spawned, 1);
224-
spawned.setViewRange(20.0F);
225-
spawned.setShadowRadius(0.0F);
226-
spawned.setShadowStrength(0.0F);
227-
spawned.setDisplayWidth(0.45F * DIE_SCALE);
228-
spawned.setDisplayHeight(0.45F * DIE_SCALE);
229-
spawned.setBillboard(Display.Billboard.FIXED);
230-
spawned.setBrightness(new Display.Brightness(15, 15));
231-
spawned.setItemStack(diceItem(point));
232-
spawned.setTransformation(new Transformation(
233-
this.zeroTranslation,
234-
this.identityRotation,
235-
this.dieScaleVector,
236-
this.identityRotation
237-
));
238-
});
218+
// World.spawn(Location, Class, Consumer) binds to the deprecated
219+
// org.bukkit.util.Consumer overload under the 1.20.1 dev bundle, which
220+
// Paper 1.21+ has removed. Use the long-stable two-argument spawn and
221+
// configure the entity afterwards. See DisplayEntities for the same fix.
222+
ItemDisplay spawned = this.world.spawn(location, ItemDisplay.class);
223+
spawned.setPersistent(false);
224+
spawned.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.HEAD);
225+
spawned.setInterpolationDuration(1);
226+
spawned.setInterpolationDelay(0);
227+
PaperCompatibility.setTeleportDuration(spawned, 1);
228+
spawned.setViewRange(20.0F);
229+
spawned.setShadowRadius(0.0F);
230+
spawned.setShadowStrength(0.0F);
231+
spawned.setDisplayWidth(0.45F * DIE_SCALE);
232+
spawned.setDisplayHeight(0.45F * DIE_SCALE);
233+
spawned.setBillboard(Display.Billboard.FIXED);
234+
spawned.setBrightness(new Display.Brightness(15, 15));
235+
spawned.setItemStack(diceItem(point));
236+
spawned.setTransformation(new Transformation(
237+
this.zeroTranslation,
238+
this.identityRotation,
239+
this.dieScaleVector,
240+
this.identityRotation
241+
));
242+
return spawned;
239243
}
240244

241245
private Location baseLocation(double xOffset) {

src/test/kotlin/top/ellan/mahjong/architecture/ArchitectureBoundaryTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,35 @@ class ArchitectureBoundaryTest {
173173
)
174174
}
175175

176+
@Test
177+
fun `World spawn callers do not use the removed Consumer overload`() {
178+
// Paper 1.21+ removed the deprecated org.bukkit.util.Consumer overload of
179+
// World.spawn(Location, Class, Consumer). Lambdas compiled against the 1.20
180+
// dev bundle were silently bound to that removed overload, breaking every
181+
// render tick on 1.21+ servers (see commit history around v1.1.1). To prevent
182+
// a regression, reject any call site that supplies a third argument to
183+
// World.spawn(Location, Class, ...). The portable two-argument form is fine.
184+
val pattern = Regex("""\.spawn\s*\(\s*[^)]*\.class\s*,\s*[A-Za-z_]""")
185+
val violations = mutableListOf<String>()
186+
sourceFiles(
187+
"src/main/java",
188+
"src/main/kotlin"
189+
).forEach { file ->
190+
val text = Files.readString(file)
191+
// Slice line by line so the violation report points at the offending site.
192+
text.lineSequence().forEachIndexed { index, line ->
193+
if (pattern.containsMatchIn(line)) {
194+
violations += "${projectRoot.relativize(file)}:${index + 1}: $line"
195+
}
196+
}
197+
}
198+
199+
assertTrue(
200+
violations.isEmpty(),
201+
"Use World.spawn(Location, Class) and configure the entity afterwards; the Consumer overload was removed in Paper 1.21:\n${violations.joinToString("\n")}"
202+
)
203+
}
204+
176205
private companion object {
177206
data class LineBudget(val path: String, val maxLines: Int)
178207
data class PublicMethodBudget(val path: String, val maxPublicMembers: Int)

0 commit comments

Comments
 (0)